2bdb1204fe
Backport of #1018 to the curated channel; the reporter on #1015 is running v3.45.14. The slideshow resolved its image as preview_url || hero_url || url. preview_url is only emitted when lightbox_preview_enabled is on (default false), so a default install fell through to hero_url — the 1920x1080 fit:'cover' centre crop built for gallery header banners. object-fit: contain then letterboxed an already-cropped 16:9 frame. Emits slideshow_url (same aspect-preserved preview tier) unconditionally for image photos; the show prefers it and never falls back to hero_url. preview_url stays gated so the lightbox opt-in is unchanged.
151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
/**
|
|
* Slideshow photo source (#1015).
|
|
*
|
|
* The bug: with `lightbox_preview_enabled` off (the default), /photos emitted
|
|
* `preview_url: null`, so the slideshow's `preview_url || hero_url || url`
|
|
* chain fell through to `hero_url` — a 1920x1080 `fit: 'cover'` centre crop
|
|
* meant for gallery header banners. With the "Black Bars (No crop)" fit the
|
|
* show then letterboxed an already-cropped frame: portrait photos lost their
|
|
* top and bottom and the setting looked broken.
|
|
*
|
|
* The contract pinned here: `slideshow_url` points at the aspect-preserved
|
|
* preview tier and is emitted for image photos REGARDLESS of the lightbox
|
|
* toggle, so the slideshow never has a reason to reach for `hero_url`.
|
|
* `preview_url` itself must stay gated — the lightbox opt-in is unchanged.
|
|
*/
|
|
|
|
const request = require('supertest');
|
|
const express = require('express');
|
|
const cookieParser = require('cookie-parser');
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
const { bootCrmDb, seedMinimal } = require('./helpers/crmDb');
|
|
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'slideshow-src-test-secret';
|
|
|
|
const SLUG = 'slideshow-source-event';
|
|
|
|
describe('Slideshow photo source (#1015)', () => {
|
|
let db;
|
|
let cleanup;
|
|
let app;
|
|
let eventId;
|
|
let imagePhotoId;
|
|
let videoPhotoId;
|
|
|
|
const galleryToken = () => jwt.sign(
|
|
{ eventId, eventSlug: SLUG, type: 'gallery' },
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '1h', issuer: 'picpeak-auth' }
|
|
);
|
|
|
|
const setLightboxPreview = async (on) => {
|
|
await db('app_settings').where({ setting_key: 'lightbox_preview_enabled' }).del();
|
|
await db('app_settings').insert({
|
|
setting_key: 'lightbox_preview_enabled',
|
|
setting_value: JSON.stringify(on),
|
|
setting_type: 'general',
|
|
updated_at: new Date().toISOString(),
|
|
});
|
|
};
|
|
|
|
const fetchPhotos = async () => {
|
|
const res = await request(app)
|
|
.get(`/api/gallery/${SLUG}/photos`)
|
|
.set('Authorization', `Bearer ${galleryToken()}`)
|
|
.expect(200);
|
|
return res.body.photos;
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
await seedMinimal(db);
|
|
|
|
const inserted = await db('events').insert({
|
|
slug: SLUG,
|
|
event_type: 'wedding',
|
|
event_name: 'Slideshow Source Test',
|
|
event_date: '2026-08-01',
|
|
host_email: 'host@example.com',
|
|
admin_email: 'admin@example.com',
|
|
password_hash: 'x',
|
|
share_link: `/gallery/${SLUG}/share`,
|
|
share_token: 'slideshow-source-share',
|
|
expires_at: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(),
|
|
is_active: 1,
|
|
is_archived: 0,
|
|
is_draft: 0,
|
|
created_at: new Date().toISOString(),
|
|
}).returning('id');
|
|
eventId = inserted[0]?.id ?? inserted[0];
|
|
|
|
const img = await db('photos').insert({
|
|
event_id: eventId,
|
|
filename: 'portrait.jpg',
|
|
path: 'events/slideshow-source/portrait.jpg',
|
|
type: 'individual',
|
|
mime_type: 'image/jpeg',
|
|
uploaded_at: new Date().toISOString(),
|
|
}).returning('id');
|
|
imagePhotoId = img[0]?.id ?? img[0];
|
|
|
|
const vid = await db('photos').insert({
|
|
event_id: eventId,
|
|
filename: 'clip.mp4',
|
|
path: 'events/slideshow-source/clip.mp4',
|
|
type: 'individual',
|
|
media_type: 'video',
|
|
mime_type: 'video/mp4',
|
|
uploaded_at: new Date().toISOString(),
|
|
}).returning('id');
|
|
videoPhotoId = vid[0]?.id ?? vid[0];
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
app.use('/api/gallery', require('../../src/routes/gallery'));
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
if (cleanup) await cleanup();
|
|
});
|
|
|
|
it('emits slideshow_url for image photos even when lightbox previews are OFF', async () => {
|
|
await setLightboxPreview(false);
|
|
const photos = await fetchPhotos();
|
|
const image = photos.find((p) => p.id === imagePhotoId);
|
|
|
|
expect(image.slideshow_url).toBe(`/api/gallery/${SLUG}/preview/${imagePhotoId}`);
|
|
// The regression: this is what used to be null, pushing the show to hero.
|
|
expect(image.preview_url).toBeNull();
|
|
});
|
|
|
|
it('leaves preview_url gated so the lightbox opt-in is unchanged', async () => {
|
|
await setLightboxPreview(true);
|
|
const photos = await fetchPhotos();
|
|
const image = photos.find((p) => p.id === imagePhotoId);
|
|
|
|
expect(image.preview_url).toBe(`/api/gallery/${SLUG}/preview/${imagePhotoId}`);
|
|
expect(image.slideshow_url).toBe(image.preview_url);
|
|
});
|
|
|
|
it('never points the slideshow at the cover-cropped hero tier', async () => {
|
|
await setLightboxPreview(false);
|
|
const photos = await fetchPhotos();
|
|
const image = photos.find((p) => p.id === imagePhotoId);
|
|
|
|
// hero_url still ships (the gallery header uses it) — it just must not be
|
|
// what the slideshow resolves to.
|
|
expect(image.hero_url).toBe(`/api/gallery/${SLUG}/hero/${imagePhotoId}`);
|
|
expect(image.slideshow_url).not.toBe(image.hero_url);
|
|
});
|
|
|
|
it('emits slideshow_url: null for videos, which have no preview tier', async () => {
|
|
await setLightboxPreview(false);
|
|
const photos = await fetchPhotos();
|
|
const video = photos.find((p) => p.id === videoPhotoId);
|
|
|
|
expect(video.slideshow_url).toBeNull();
|
|
});
|
|
});
|