fix(studio): lint and test fixes: zoom bar labels without the studio object, tile zooms in a helper, tests without non-null assertions or the window toast

This commit is contained in:
Maurice
2026-10-24 10:07:26 +02:00
parent 44ea1ea0ba
commit 92be8a7ca5
6 changed files with 38 additions and 27 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"src": {
"@typescript-eslint/no-non-null-assertion": 385,
"@typescript-eslint/no-non-null-assertion": 384,
"@typescript-eslint/no-misused-promises": 849,
"no-console": 88,
"complexity": 222,
@@ -102,10 +102,10 @@ describe('StudioHeader', () => {
it('FE-STUDIO-HEAD-006: auto layout offers this spread and the whole book', () => {
const { s } = head();
fireEvent.click(screen.getByRole('button', { name: /journey.studio.autoLayout/ }));
fireEvent.click(screen.getByRole('menuitem', { name: /journey.studio.relayoutSpread\b/ }));
fireEvent.click(screen.getByRole('menuitem', { name: /^journey\.studio\.relayoutSpread/ }));
expect(s.relayoutCurrentSpread).toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: /journey.studio.autoLayout/ }));
fireEvent.click(screen.getByRole('menuitem', { name: /journey.studio.relayoutBook\b/ }));
fireEvent.click(screen.getByRole('menuitem', { name: /^journey\.studio\.relayoutBook/ }));
expect(s.relayoutBook).toHaveBeenCalled();
});
+4 -2
View File
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
import { useNavigate } from 'react-router'
import { BookOpen, Download, Maximize2, Minus, Plus } from 'lucide-react'
import { useJourneyStudio } from '../../pages/journeyStudio/useJourneyStudio'
import { useTranslation } from '../../i18n'
import { foliosOf } from './bookSheets'
import { StudioSidebar } from './StudioSidebar'
import type { RailSection } from './sidebar/railSections'
@@ -153,6 +154,7 @@ export default function StudioShell() {
export type Studio = ReturnType<typeof useJourneyStudio>
function Workbench({ s, bookView, setBookView }: { s: Studio; bookView: boolean; setBookView: (on: boolean) => void }) {
const { t } = useTranslation()
return (
<div className="st-work-wrap">
<div className="st-work" ref={s.workRef}>
@@ -198,8 +200,8 @@ function Workbench({ s, bookView, setBookView }: { s: Studio; bookView: boolean;
<button type="button"
onClick={() => setBookView(!bookView)}
aria-pressed={bookView}
aria-label={s.t('journey.studio.bookView')}
title={s.t('journey.studio.bookView')}
aria-label={t('journey.studio.bookView')}
title={t('journey.studio.bookView')}
className={bookView ? 'is-on' : undefined}
>
<BookOpen size={14} />
+15 -9
View File
@@ -126,20 +126,26 @@ function enumerateTileUrls(bbox: TileBbox, sources: TileSource[]): string[] {
const out: string[] = []
for (const source of sources) {
for (let z = Math.max(source.minZoom, VECTOR_MIN_ZOOM); z <= Math.min(source.maxZoom, VECTOR_MAX_ZOOM); z++) {
// Same screen-widened rectangle as the raster side: the opening fitBounds
// view shows more than the places extent on the non-limiting axis (#2180).
const { minX, maxX, minY, maxY } = tileRange(bbox, z)
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
out.push(source.template.replace('{z}', String(z)).replace('{x}', String(x)).replace('{y}', String(y)))
if (out.length >= MAX_VECTOR_TILES) return out
}
}
if (!pushZoomTiles(out, source.template, bbox, z)) return out
}
}
return out
}
/** One zoom's tiles of a source onto `out`; false once the run is full. */
function pushZoomTiles(out: string[], template: string, bbox: TileBbox, z: number): boolean {
// Same screen-widened rectangle as the raster side: the opening fitBounds
// view shows more than the places extent on the non-limiting axis (#2180).
const { minX, maxX, minY, maxY } = tileRange(bbox, z)
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
out.push(template.replace('{z}', String(z)).replace('{x}', String(x)).replace('{y}', String(y)))
if (out.length >= MAX_VECTOR_TILES) return false
}
}
return true
}
/**
* The style's tiled sources, vector first. A source given as a TileJSON
* reference gets its template and zooms from there: the tile path carries a
@@ -2,6 +2,10 @@ import { act, render } from '@testing-library/react';
import { MAX_SPREAD_ELEMENTS, type BookDocument, type BookElement, type BookSpread } from '@trek/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const toast = vi.hoisted(() => ({ warning: vi.fn() }));
vi.mock('../../../src/components/shared/Toast', () => ({ useToast: () => toast }));
vi.mock('../../../src/i18n', () => ({ useTranslation: () => ({ t: (key: string) => key }) }));
import { StudioNotices } from '../../../src/components/Studio/StudioNotices';
import { useStudioStore } from '../../../src/store/studioStore';
@@ -56,7 +60,7 @@ beforeEach(() => {
describe('a full page', () => {
it('FE-STUDIO-FULL-001: refuses one more element and counts the refusal', () => {
expect(store().addElement(0, el('extra'))).toBe(false);
expect(store().doc!.spreads[0].elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().doc?.spreads[0]?.elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().refusedFull).toBe(1);
expect(store().addElement(1, el('fits'))).toBe(true);
expect(store().refusedFull).toBe(1);
@@ -65,25 +69,24 @@ describe('a full page', () => {
it('FE-STUDIO-FULL-002: a paste onto it is refused and counted', () => {
useStudioStore.setState({ clipboard: { fromSpreadId: 'room', elements: [el('a')] } });
store().paste(0);
expect(store().doc!.spreads[0].elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().doc?.spreads[0]?.elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().refusedFull).toBe(1);
});
it('FE-STUDIO-FULL-003: a duplicate on it is refused and counted', () => {
store().duplicate(0, ['f0']);
expect(store().doc!.spreads[0].elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().doc?.spreads[0]?.elements).toHaveLength(MAX_SPREAD_ELEMENTS);
expect(store().refusedFull).toBe(1);
});
it('FE-STUDIO-FULL-004: the Studio says it once per refusal', () => {
const addToast = vi.fn();
window.__addToast = addToast;
toast.warning.mockClear();
render(<StudioNotices />);
expect(addToast).not.toHaveBeenCalled();
expect(toast.warning).not.toHaveBeenCalled();
act(() => store().refuseFull());
expect(addToast).toHaveBeenCalledTimes(1);
expect(addToast.mock.calls[0][1]).toBe('warning');
expect(toast.warning).toHaveBeenCalledTimes(1);
expect(toast.warning).toHaveBeenLastCalledWith('journey.studio.pageFull');
act(() => store().refuseFull());
expect(addToast).toHaveBeenCalledTimes(2);
expect(toast.warning).toHaveBeenCalledTimes(2);
});
});
+4 -4
View File
@@ -147,7 +147,7 @@ describe('prefetchVectorForPlaces', () => {
const zooms = new Set(
[...cache.store.keys()]
.filter(u => u.endsWith('.pbf') && u.includes('/planet/'))
.map(u => Number(u.split('/planet/')[1]!.split('/')[1])),
.map(u => Number((u.split('/planet/')[1] ?? '').split('/')[1])),
);
// z14 is the last one OpenFreeMap serves; MapLibre scales it beyond that
// rather than asking for another, which is why this stops where raster could not.
@@ -166,7 +166,7 @@ describe('prefetchVectorForPlaces', () => {
const xs = [...cache.store.keys()]
.filter(u => u.endsWith('.pbf') && u.includes('/planet/'))
.map(u => u.split('/planet/')[1]!.split('/'))
.map(u => (u.split('/planet/')[1] ?? '').split('/'))
.filter(parts => Number(parts[1]) === 7)
.map(parts => Number(parts[2]));
expect((Math.max(...xs) - Math.min(...xs) + 1) * 256).toBeGreaterThanOrEqual(window.innerWidth);
@@ -233,9 +233,9 @@ describe('a style with a raster source ahead of the vector one', () => {
const result = await prefetchVectorForPlaces(places, STYLE_URL);
expect(result.template).toBe(TILE_TEMPLATE);
const urls = requested.map(r => r.url);
const vectorZooms = new Set(urls.filter(u => u.includes('/planet/')).map(u => Number(u.split('/planet/')[1]!.split('/')[1])));
const vectorZooms = new Set(urls.filter(u => u.includes('/planet/')).map(u => Number((u.split('/planet/')[1] ?? '').split('/')[1])));
expect(Math.max(...vectorZooms)).toBe(14);
const reliefZooms = urls.filter(u => u.includes('/ne2sr/')).map(u => Number(u.split('/ne2sr/')[1]!.split('/')[0]));
const reliefZooms = urls.filter(u => u.includes('/ne2sr/')).map(u => Number((u.split('/ne2sr/')[1] ?? '').split('/')[0]));
expect(reliefZooms.length).toBeGreaterThan(0);
expect(Math.max(...reliefZooms)).toBe(6);
});