mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-25 00:13:00 +00:00
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:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user