Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc5b6955d8 | ||
|
|
35752a7d53 | ||
|
|
e48469b058 | ||
|
|
660d84502a | ||
|
|
ff606dad71 |
@@ -0,0 +1,28 @@
|
||||
# TEMPORARY diagnostic workflow — delete after the rankings year= bug is closed.
|
||||
name: Staging Rankings Diagnostic
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- diag/staging-rankings-year
|
||||
|
||||
jobs:
|
||||
staging-api-diagnostic:
|
||||
name: Staging rankings year diagnostic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Probe staging rankings year handling
|
||||
env:
|
||||
BASE: ${{ secrets.STAGING_BASE_URL }}
|
||||
run: |
|
||||
probe() {
|
||||
echo "== $1 =="
|
||||
curl -s --max-time 15 -D /tmp/h.txt -o /tmp/b.txt "$BASE$1"
|
||||
echo "--- headers:"; cat /tmp/h.txt
|
||||
echo "--- body (first 400 bytes):"; head -c 400 /tmp/b.txt; echo
|
||||
}
|
||||
probe "/api/data-info"
|
||||
probe "/api/filters"
|
||||
probe "/api/rankings?metric=rwm_expected_pct&limit=3"
|
||||
probe "/api/rankings?metric=rwm_expected_pct&limit=3&year=202425"
|
||||
probe "/"
|
||||
@@ -12,7 +12,25 @@ env:
|
||||
PIPELINE_IMAGE_NAME: ${{ gitea.repository }}-pipeline
|
||||
|
||||
jobs:
|
||||
frontend-checks:
|
||||
# TEMPORARY: evidence gathering for the rankings year= empty-list bug on
|
||||
# staging. Remove before merging. Prints status codes and row counts only.
|
||||
staging-api-diagnostic:
|
||||
name: Staging rankings year diagnostic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Probe staging rankings year handling
|
||||
env:
|
||||
BASE: ${{ secrets.STAGING_BASE_URL }}
|
||||
run: |
|
||||
echo "== /api/filters years =="
|
||||
curl -s --max-time 15 "$BASE/api/filters" -o /tmp/f.json -w "status %{http_code}\n"
|
||||
python3 -c "import json; print(json.load(open('/tmp/f.json')).get('years'))" || head -c 300 /tmp/f.json
|
||||
echo "== /api/rankings probes (metric=rwm_expected_pct, limit=3) =="
|
||||
for Q in "" "&year=202425" "&year=201819" "&year=2024"; do
|
||||
CODE=$(curl -s --max-time 15 -o /tmp/r.json -w "%{http_code}" "$BASE/api/rankings?metric=rwm_expected_pct&limit=3$Q")
|
||||
echo "query [$Q] -> status $CODE"
|
||||
python3 -c "import json; d=json.load(open('/tmp/r.json')); print(' year:', d.get('year'), 'total:', d.get('total'), 'rows:', len(d.get('rankings', [])))" || head -c 300 /tmp/r.json
|
||||
done
|
||||
name: Frontend Typecheck + Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -50,6 +50,31 @@ test('school detail page renders name and performance data', async ({ page }) =>
|
||||
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('school hero map opens fullscreen on mobile without the Fullscreen API', async ({ page }) => {
|
||||
// iOS Safari has no Element.requestFullscreen; the map must fall back to a
|
||||
// CSS overlay. Simulate that by removing the API before any page script runs.
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.addInitScript(() => {
|
||||
// @ts-expect-error deliberate API removal
|
||||
delete Element.prototype.requestFullscreen;
|
||||
});
|
||||
|
||||
await searchByName(page, 'primary');
|
||||
const firstSchool = schoolLinks(page).first();
|
||||
await expect(firstSchool).toBeVisible({ timeout: 15_000 });
|
||||
await firstSchool.click();
|
||||
await page.waitForURL(/\/school\//);
|
||||
|
||||
const openMap = page.getByRole('button', { name: 'Open full map' });
|
||||
await expect(openMap).toBeVisible({ timeout: 15_000 });
|
||||
await openMap.click();
|
||||
|
||||
const closeMap = page.getByRole('button', { name: 'Close map' });
|
||||
await expect(closeMap).toBeVisible();
|
||||
await closeMap.click();
|
||||
await expect(openMap).toBeVisible();
|
||||
});
|
||||
|
||||
test('comparing two schools shows both side by side', async ({ page }) => {
|
||||
// Collect two school URNs from search results, then load the share URL
|
||||
await searchByName(page, 'primary');
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Fallback fullscreen (iOS Safari — no Element.requestFullscreen): the API
|
||||
can't promote the element, so pin it over the page ourselves. Above the
|
||||
comparison toast (3000) and everything else except modals (9999+). */
|
||||
.wrapper[data-fs-fallback] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -29,25 +29,50 @@ interface SchoolHeroMapProps {
|
||||
export const SchoolHeroMap = forwardRef<SchoolHeroMapHandle, SchoolHeroMapProps>(
|
||||
function SchoolHeroMap({ lat, lng }, ref) {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [nativeFullscreen, setNativeFullscreen] = useState(false);
|
||||
// iOS Safari has no Element.requestFullscreen — fall back to a
|
||||
// fixed-position overlay driven by state instead of the Fullscreen API.
|
||||
const [fallbackFullscreen, setFallbackFullscreen] = useState(false);
|
||||
const isFullscreen = nativeFullscreen || fallbackFullscreen;
|
||||
|
||||
const open = useCallback(() => {
|
||||
wrapperRef.current?.requestFullscreen?.().catch(() => {});
|
||||
const el = wrapperRef.current;
|
||||
if (!el) return;
|
||||
if (el.requestFullscreen) {
|
||||
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
|
||||
} else {
|
||||
setFallbackFullscreen(true);
|
||||
}
|
||||
}, []);
|
||||
const close = useCallback(() => {
|
||||
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
|
||||
setFallbackFullscreen(false);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({ open }), [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
const onChange = () => setNativeFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', onChange);
|
||||
return () => document.removeEventListener('fullscreenchange', onChange);
|
||||
}, []);
|
||||
|
||||
// The fallback overlay sits on top of the page rather than replacing it,
|
||||
// so lock body scroll while it is up.
|
||||
useEffect(() => {
|
||||
if (!fallbackFullscreen) return;
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prev; };
|
||||
}, [fallbackFullscreen]);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={styles.wrapper} data-fullscreen={isFullscreen || undefined}>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={styles.wrapper}
|
||||
data-fullscreen={isFullscreen || undefined}
|
||||
data-fs-fallback={fallbackFullscreen || undefined}
|
||||
>
|
||||
<LeafletHeroMap lat={lat} lng={lng} interactive={isFullscreen} />
|
||||
|
||||
{isFullscreen ? (
|
||||
|
||||
Reference in New Issue
Block a user