File size: 3,635 Bytes
50841a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
const DEFAULT_REPORTING = {
  max_html_issues: 50,
  moderate_issue_threshold_percent: 5,
  include_key_page_issues: true,
};

const SEVERITY_RANK = { Critical: 0, High: 1, Medium: 2, Low: 3 };

export function normalizeAccessibilityReporting(reporting = {}) {
  return {
    ...DEFAULT_REPORTING,
    ...reporting,
    max_html_issues: Math.max(0, Number(reporting.max_html_issues ?? DEFAULT_REPORTING.max_html_issues) || 0),
    moderate_issue_threshold_percent: Math.max(
      0,
      Number(reporting.moderate_issue_threshold_percent ?? DEFAULT_REPORTING.moderate_issue_threshold_percent) || 0
    ),
    include_key_page_issues: reporting.include_key_page_issues ?? DEFAULT_REPORTING.include_key_page_issues,
  };
}

export function prioritizeAccessibilityBugs(summary, bugs, { keyPages = [], reporting = {} } = {}) {
  const cfg = normalizeAccessibilityReporting(reporting);
  const keyPageSet = cfg.include_key_page_issues ? new Set(keyPages) : new Set();
  const totalPages = summary.pagesScanned || bugs[0]?.frequency.total_pages_scanned || 0;
  const threshold = cfg.moderate_issue_threshold_percent;

  const decorated = bugs.map((bug) => {
    const pagesAffected = bug.frequency?.pages_affected ?? 0;
    const prevalencePercent = totalPages > 0 ? (100 * pagesAffected) / totalPages : 0;
    const keyPageHit = keyPageSet.size > 0 && bugHasKeyPageHit(summary, bug, keyPageSet);
    const tier = priorityTier(bug, prevalencePercent, keyPageHit, threshold, cfg.include_key_page_issues);
    return {
      ...bug,
      priority_tier: tier,
      priority_key_page: keyPageHit,
      priority_prevalence_percent: Math.round(prevalencePercent * 100) / 100,
    };
  });

  decorated.sort((a, b) =>
    a.priority_tier - b.priority_tier ||
    (b.frequency?.pages_affected ?? 0) - (a.frequency?.pages_affected ?? 0) ||
    (SEVERITY_RANK[a.severity] ?? 99) - (SEVERITY_RANK[b.severity] ?? 99) ||
    (b.frequency?.instances ?? 0) - (a.frequency?.instances ?? 0) ||
    String(a.summary ?? '').localeCompare(String(b.summary ?? ''))
  );

  const visible = [];
  for (const bug of decorated) {
    if (bug.priority_tier <= 1) visible.push(bug);
  }
  for (const bug of decorated) {
    if (bug.priority_tier <= 1) continue;
    if (cfg.max_html_issues > 0 && visible.length >= cfg.max_html_issues) break;
    visible.push(bug);
  }

  const visibleSet = new Set(visible.map((b) => b.instance_id));
  return {
    bugs: decorated.map((bug) => ({
      ...bug,
      default_visible: visibleSet.has(bug.instance_id),
    })),
    visibleCount: visibleSet.size,
    totalCount: bugs.length,
    reporting: cfg,
  };
}

function priorityTier(bug, prevalencePercent, keyPageHit, thresholdPercent, includeKeyPages) {
  if (bug.severity === 'Critical' || bug.severity === 'High') return 0;
  if (includeKeyPages && keyPageHit && prevalencePercent > thresholdPercent) return 1;
  if (isWcagAorAa(bug.wcag_category)) return 2;
  if (bug.wcag_category === 'Best Practice') return 3;
  if (bug.wcag_category === 'WCAG 2.x AAA') return 4;
  return 5;
}

function isWcagAorAa(category) {
  return /^WCAG \d\.\d [A]{1,2}$/.test(category ?? '');
}

function bugHasKeyPageHit(summary, bug, keyPageSet) {
  const rule = ruleForBug(summary, bug);
  const affected = rule?.affectedPages?.map((p) => p.url).filter(Boolean) ?? bug.affected_pages ?? [];
  return affected.some((url) => keyPageSet.has(url));
}

function ruleForBug(summary, bug) {
  const rules = {
    'axe-core': summary.axe?.rules,
    alfa: summary.alfa?.rules,
    'deprecated-html': summary.deprecatedHtml?.rules,
  }[bug.engine_key];
  return rules?.[bug.rule_id] ?? null;
}