Files
advanced-camera-card/tests/components/live/carousel.browser.test.ts
T
Matthijsanddermotduffy 299973a1fa fix: size a newly selected grid cell in a single layout pass (#2730)
Selecting a camera in `live.display.mode: grid` lands in two visible
passes: the cell gets its 2-column width immediately but keeps its 1-row
height for ~400 ms, and the rest of the grid only settles ~700 ms after
the click (measured per-frame on `main`; v7.27.4 has the same two
passes, v8's early position resolution just leaves the late height
correction standing out as a lone vertical jump).

**Cause:** a grid cell's height follows its content in the same style
recalculation as its width (intrinsic media ratio,
`dimensions.aspect_ratio`, or the unsized 16:9 reservation), but the
slotted carousel carries an inline `max-height` from
`MediaHeightController` (debounced 0.3 s + 0.1 s transition; it sizes
the card outside grids). In a grid that cap can only ever delay growth:
it pins the freshly widened cell at its old height until the debounce
fires, and the correction then also rides the 300 ms `_throttledLayout`.
`selectCell()`'s existing `forceReflow()` + `layout()` was already
positioned to do this in one pass; the cap starves it of the final
height.

**Fix:** neutralize the cap on grid cells (`max-height: none
!important`; important is needed to beat the inline style). Covers the
live and viewer grids; non-grid behaviour is untouched. This also stops
the cap clipping cells by their border width (it was measured on the
slide's content box but applied to the cell's border box).

**Trade-off worth flagging:** in the *viewer* grid a cell's carousel
holds all of one camera's media. If those slides do not share one ratio,
the cell now sizes to its tallest slide (letterboxing shorter ones, no
re-layout per swipe) instead of tracking the selected one after a
debounce. If you would rather leave the viewer untouched, the rule can
be scoped to `::slotted(advanced-camera-card-live-carousel)` instead
(live grid cells always hold exactly one slide); say so and I will
rework the PR that way.

**Verification:**

- New browser test `tests/components/live/grid.browser.test.ts`: fails
on any frame where the newly selected cell is selected-wide but still
unselected-high. On `main` it fails with 22 such frames; with this
change there are none: the click settles in a single frame (~28 ms),
every cell at its final size and position.
- `yarn run test`, `yarn run test:browser` (chromium, firefox and webkit
for the new test), `yarn run lint`, `yarn run typecheck` pass.
- No resize oscillation in a cramped viewport with an
appearing/disappearing ancestor scrollbar (the #2306 scenario), on
window resizes, or under a narrow-screen `grid_columns: 2` override;
verified against a live HA 2026.8.3 dashboard (6 go2rtc cameras).

---------

Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
2026-08-30 17:33:48 -07:00

36 lines
1.1 KiB
TypeScript

import { assert, describe, expect, it } from 'vitest';
import { deepQuery } from '../../browser/dom';
import { MountedCardFactory } from '../../browser/mounted-card';
import {
createGenericCameraHASS,
createStillImageCardConfig,
} from '../../browser/test-utils';
describe('AdvancedCameraCardLiveCarousel', () => {
it('should cap its own height to fit the media it shows', async () => {
const card = await MountedCardFactory.createFromSource(
createStillImageCardConfig(),
createGenericCameraHASS(),
);
await card.events.waitForFirst('advanced-camera-card:media:loaded');
const carousel = deepQuery<HTMLElement>(
card.card,
'advanced-camera-card-live-carousel',
);
assert(carousel);
await card.waitForRender(
() => carousel.style.maxHeight || null,
'the carousel capping its own height',
);
// Outside a grid the carousel fills the card, so the cap is what gives the
// card the height of its media rather than of whatever contains it.
expect(parseFloat(carousel.style.maxHeight)).toBe(
carousel.getBoundingClientRect().height,
);
});
});