diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35e62509..53fa0e68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: node scripts/rewrite-links.mjs node scripts/smoke-test.mjs node scripts/seat-color-test.mjs + node scripts/seatmap-ui-test.mjs + node scripts/seat-center-zone-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 3e92d0d1..85d58035 100644 --- a/.github/workflows/pages-stage.yml +++ b/.github/workflows/pages-stage.yml @@ -87,6 +87,8 @@ jobs: node scripts/rewrite-links.mjs node scripts/smoke-test.mjs node scripts/seat-color-test.mjs + node scripts/seatmap-ui-test.mjs + node scripts/seat-center-zone-test.mjs node scripts/seo-audit.mjs - name: Stage full site and preview into gh-pages diff --git a/scripts/seat-center-zone-test.mjs b/scripts/seat-center-zone-test.mjs new file mode 100644 index 00000000..9061efba --- /dev/null +++ b/scripts/seat-center-zone-test.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { estimateCenterZone } from '../src/lib/seat-center-zone.js'; + +const createRow = (y, xs, occupied = false) => ({ + seats: xs.map(x=>({x,y,width:27,height:27,occupied})) +}); + +// Reproduces the reported CGV layout: eight contiguous rows A-H, then a +// detached, smaller J-K block separated by a walkable aisle. The old algorithm +// stretched the center frame from D to J; the new one MUST stop by H. +const mainXs = [300,334,368,402,436,470,504,538]; +const frontBank = Array.from({length:8},(_,i)=>createRow(i*35,mainXs,i>4)); +const rearBank = [createRow(365,[140,174,...mainXs]),createRow(400,[140,174,...mainXs])]; +const cg = estimateCenterZone([...frontBank,...rearBank]); +assert.ok(cg, 'CGV center frame should exist'); +assert.equal(cg.blockCount,2,'Detached back-row bank must not merge into main'); +assert.equal(cg.mainRowCount,8,'A-H are the main continuous block'); +assert.equal(cg.firstMainRow,3,'Central rows should start around row D'); +assert.equal(cg.lastMainRow,7,'Central rows must stop at row H'); +assert.ok(cg.y>frontBank[2].seats[0].y,'Frame starts behind front rows'); +assert.ok(cg.y+cg.height285 && cg.x+cg.width<555,'Frame stays near central occupied seats'); +assert.ok(cg.width>40 && cg.height>70,'Frame should cover meaningful seats'); + +// All-occupied geometry must produce the same location; availability cannot +// change which seating area is objectively in the middle. +const blocked = estimateCenterZone([...frontBank,...rearBank].map(r => ({ + seats: r.seats.map(s=>({...s,occupied:true})) +}))); +assert.deepEqual(blocked,cg,'Seat occupancy cannot alter central-zone position'); + +// A normal theater with no aisle produces one meaningful box. +const standard = estimateCenterZone( + Array.from({length:11},(_,i)=>createRow(i*34,Array.from({length:12},(_,j)=>j*34))) +); +assert.ok(standard && standard.blockCount===1); +assert.ok(standard.x>70 && standard.x+standard.width<340); +assert.ok(standard.lastMainRow<11); + +// A rear block with more seats than the front should be selected on its own, +// rather than combining separated seating sections into an enormous rectangle. +const rearBig = [ + ...[0,35,70,105].map(y=>createRow(y,[80,115,150,185,220,255])), + ...[200,235,270,305,340,375,410].map(y=>createRow(y,[0,35,70,105,140,175,210,245,280,315])) +]; +const rearZone=estimateCenterZone(rearBig); +assert.equal(rearZone.blockCount,2); +assert.equal(rearZone.mainRowCount,7); +assert.ok(rearZone.y>=200,'Choose main contiguous rear block, without crossing aisle'); + +// Do not draw misleading pseudo-precision for tiny or malformed seating plans. +assert.equal(estimateCenterZone([]),null); +assert.equal(estimateCenterZone([createRow(0,[1]),createRow(35,[1])]),null); +assert.equal(estimateCenterZone([{seats:[{x:NaN,y:1,width:1,height:1}]}]),null); +console.log('PASS: center frame avoids the H-J aisle, uses the main bank, and ignores occupancy'); diff --git a/scripts/seatmap-ui-test.mjs b/scripts/seatmap-ui-test.mjs new file mode 100644 index 00000000..05b6ec54 --- /dev/null +++ b/scripts/seatmap-ui-test.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const component = readFileSync('src/components/SeatMap.astro', 'utf8'); +const style = readFileSync('src/styles/seatmap-redesign.css', 'utf8'); + +for (const control of ['zoom-out', 'zoom-in', 'zoom-fit', 'zoom-reset', 'zoom-value']) { + assert.ok(component.includes('data-seatmap-' + control), 'Missing zoom control: ' + control); +} +assert.match(component, /const ZOOM_MIN = 0\.45;/, 'Minimum zoom must be bounded'); +assert.match(component, /const ZOOM_MAX = 1\.8;/, 'Maximum zoom must be bounded'); +assert.match(component, /stage\.style\.zoom = String\(next\)/, 'Zoom must affect stage layout, not just transform paint'); +assert.match(component, /touchmove/, 'Touch pinch handler missing'); +assert.match(component, /passive: false/, 'Pinch should prevent native zoom of the whole page'); +assert.match(component, /root\.parentElement !== document\.body/, 'Modal needs to escape parent stacking context'); +assert.match(style, /z-index: 1200/, 'Modal must overlay sticky mobile header'); +assert.match(component, /data-seatmap-center-zone/, 'Center frame geometry missing'); +assert.match(style, /pointer-events: none/, 'Center frame must allow clicking seats'); +assert.match(style, /\.seatmap__swatch--center/, 'Legend lacks center zone outline'); +assert.match(component, /Giữa dãy ghế chính \(ước lượng\)/, 'Do not claim an official provider central-seat classification'); +assert.match(component, /estimateCenterZone\(seatRows\)/, 'Use aisle-aware center seat estimator'); +assert.match(component, /button\.classList\.add\('seat--selected'\)/, 'Selecting must apply dedicated CSS class'); +assert.match(component, /button\.classList\.remove\('seat--selected'\)/, 'Deselecting must remove dedicated CSS class'); +assert.match(component, /button\.setAttribute\('aria-pressed', 'true'\)/, 'Selected seat must remain accessible'); +assert.match(style, /\.seatmap \.seat\.seat--selected[\s\S]*?background: #19b3a8 !important/, 'Teal must always override provider type color'); +assert.ok(!component.includes('Mua vé'), 'Preview cannot imply it purchases tickets'); + +console.log('PASS: seat map zoom, selection color, central zone geometry and mobile modal precedence'); diff --git a/src/components/SeatMap.astro b/src/components/SeatMap.astro index eedb425e..77c5562d 100644 --- a/src/components/SeatMap.astro +++ b/src/components/SeatMap.astro @@ -19,6 +19,16 @@ import '../styles/seatmap-redesign.css'; +
@@ -27,7 +37,7 @@ import '../styles/seatmap-redesign.css';