diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3187d3f..bab62122 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: run: | node scripts/rewrite-links.mjs node scripts/smoke-test.mjs + node scripts/seat-color-test.mjs node scripts/seo-audit.mjs node scripts/check-outbound-links.mjs - name: Verify explicitly indexable build diff --git a/.github/workflows/pages-stage.yml b/.github/workflows/pages-stage.yml index 23cf9eb2..3e92d0d1 100644 --- a/.github/workflows/pages-stage.yml +++ b/.github/workflows/pages-stage.yml @@ -86,6 +86,7 @@ jobs: npm run build node scripts/rewrite-links.mjs node scripts/smoke-test.mjs + node scripts/seat-color-test.mjs node scripts/seo-audit.mjs - name: Stage full site and preview into gh-pages diff --git a/scripts/seat-color-test.mjs b/scripts/seat-color-test.mjs new file mode 100644 index 00000000..3ff14619 --- /dev/null +++ b/scripts/seat-color-test.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { parseSeatColor, seatForeground, seatTypePalette, seatColorStyle } from '../src/lib/seat-colors.js'; + +// Source-supplied names do not imply a color; the Color field does. +const sourceTypes = [ + { Type: 1, Name: 'Ghế Thường', Color: '#7f55db' }, + { Type: 2, Name: 'VIP', Color: 'rgb(236, 62, 83)' }, + { Type: 3, Name: 'WAKU WAKU', Color: '#e4a43e' }, + { Type: 4, Name: 'DOKI DOKI', Color: '#e64f9e' } +]; +const palette = seatTypePalette(sourceTypes); +assert.equal(palette.size, 4); +assert.equal(palette.get('1').color, '#7f55db'); +assert.equal(palette.get('2').color, '#ec3e53'); +assert.equal(palette.get('3').color, '#e4a43e'); +assert.equal(palette.get('4').color, '#e64f9e'); +assert.equal(new Set([...palette.values()].map((type) => type.color)).size, 4); +assert.equal(seatForeground('#ffffff'), '#132033'); +assert.equal(seatForeground('#151515'), '#ffffff'); + +// Repeated/missing/invalid colors must not collapse two distinct seat types. +const ambiguousTypes = seatTypePalette([ + { Type: '1', Name: 'Ghế Thường', Color: '#7f55db' }, + { Type: '2', Name: 'WAKU WAKU', Color: '#7f55db' }, + { Type: '3', Name: 'DOKI DOKI', Color: '' }, + { Type: '4', Name: 'Khác', Color: 'red;position:absolute' } +]); +assert.equal(ambiguousTypes.size, 4); +assert.equal(new Set([...ambiguousTypes.values()].map((type) => type.color)).size, 4); +assert.equal(parseSeatColor('#abc'), '#aabbcc'); +assert.equal(parseSeatColor('rgba(0,0,0,0)'), null); +assert.equal(parseSeatColor('url(javascript:alert(1))'), null); +assert.match(seatColorStyle(ambiguousTypes.get('4')), /^--seat-type-color:#[0-9a-f]{6};--seat-type-foreground:#[0-9a-f]{6}$/); +const markup = readFileSync('src/components/SeatMap.astro', 'utf8'); +const css = readFileSync('src/styles/seatmap-redesign.css', 'utf8'); +assert.ok(markup.includes('palette.get(String(seat.cinemaAreaCode))'), 'Seat must resolve by actual source code'); +assert.ok(markup.includes('seatColorStyle(type)'), 'Seat and legend must share the same palette'); +assert.ok(markup.includes('seatmap__counts'), 'Occupancy totals must not be presented as seat type colors'); +assert.ok(!markup.includes("const kind = /vip/"), 'Do not infer type color from names'); +assert.ok(css.includes('background: var(--seat-type-color'), 'Seat type CSS variable missing'); +assert.ok(css.includes('.seatmap__swatch--type'), 'Legend color CSS variable missing'); +console.log('PASS: seat type colors, duplicate/invalid data fallbacks, contrast, rendering and legend'); diff --git a/src/components/SeatMap.astro b/src/components/SeatMap.astro index a73cf9c8..eedb425e 100644 --- a/src/components/SeatMap.astro +++ b/src/components/SeatMap.astro @@ -105,6 +105,7 @@ import '../styles/seatmap-redesign.css';