garrytan/gstack · error · Error

Invalid heatmap color

Error message

Invalid heatmap color "${color}" for ${ref}. Valid: ${[...VALID_COLORS].join(', ')}

What it means

Thrown after a successful JSON parse when at least one value in the heatmap object is not in the allowed color set {green, yellow, red, blue, orange, gray}. The error names the offending color, the ref it was attached to, and the valid list so the caller can fix that one entry.

Solutions

  1. Replace the offending value with one of: green, yellow, red, blue, orange, gray.
  2. If you need a custom color, fork the COLOR_MAP and VALID_COLORS set in snapshot.ts — but note this changes the public contract.
  3. Lower-case the values before serializing to avoid case mismatches.
  4. Strip leading `#` and hex codes — the API takes color NAMES, not codes.

Example fix

// before
snapshot(page, { heatmap: '{"@e1":"#ff0000","@e3":"purple"}' });
// after
snapshot(page, { heatmap: '{"@e1":"red","@e3":"gray"}' });
Defensive patterns

Strategy: validation

Validate before calling

const VALID_COLORS = new Set(['green','yellow','red','blue','orange','gray']);
function validateHeatmapColors(obj: Record<string, string>): void {
  for (const [ref, color] of Object.entries(obj)) {
    if (!VALID_COLORS.has(color)) {
      throw new Error(`Invalid heatmap color "${color}" for ${ref}. Valid: ${[...VALID_COLORS].join(', ')}`);
    }
  }
}

Type guard

const isHeatmapColor = (c: string): boolean =>
  ['green','yellow','red','blue','orange','gray'].includes(c);

Try / catch

try {
  await snapshot(session, { heatmap: json });
} catch (e: any) {
  if (/^Invalid heatmap color/.test(e.message)) {
    // normalize: clamp unknown colors to gray and retry
    const fixed = remapColors(json, c => isHeatmapColor(c) ? c : 'gray');
    await snapshot(session, { heatmap: JSON.stringify(fixed) });
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a heatmap object whose value is a hex code (`'#ff0000'`), a CSS color keyword outside the allow-list (`purple`), a typo (`gren`), or any non-string type.

Common situations: Reusing a CSS palette that includes colors not in the allow-list; case mismatch (`Red` vs `red` — the check is case-sensitive); a teammate adding a custom color without realizing the set is closed.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/85b49285dd6038e8. Report an issue: GitHub.

Appendix: source

Thrown at browse/src/snapshot.ts:501

      orange: { border: '#ff6600', bg: 'rgba(255,102,0,0.15)' },
      gray:   { border: '#888888', bg: 'rgba(136,136,136,0.15)' },
    };

    let colorAssignments: Record<string, string>;
    try {
      const parsed = JSON.parse(opts.heatmap);
      if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
        throw new Error('not an object');
      }
      colorAssignments = parsed;
    } catch {
      throw new Error('Invalid heatmap JSON. Expected object: \'{"@e1":"green","@e3":"red"}\'');
    }

    // Validate colors
    for (const [ref, color] of Object.entries(colorAssignments)) {
      if (!VALID_COLORS.has(color)) {
        throw new Error(`Invalid heatmap color "${color}" for ${ref}. Valid: ${[...VALID_COLORS].join(', ')}`);
      }
    }

    try {
      const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number }; color: string }> = [];
      for (const [refKey, color] of Object.entries(colorAssignments)) {
        const cleanRef = refKey.startsWith('@') ? refKey.slice(1) : refKey;
        const entry = refMap.get(cleanRef);
        if (!entry) continue; // Skip refs not found on page
        try {
          const box = await entry.locator.boundingBox({ timeout: 1000 });
          if (box) {
            const colors = COLOR_MAP[color] || COLOR_MAP.gray;
            boxes.push({ ref: `@${cleanRef}`, box, color: JSON.stringify(colors) });
          }
        } catch {
          // Element may be offscreen or hidden — skip
        }

View on GitHub (pinned to 94993f7401)