fix(street-level): KartaView PR review — context loss mid-load, chunked reads, source switches, a11y

From the IC8 review of #1121 (2 P2, 7 P3; each with a test that fails on
the old code):

P2
- A WebGL context lost while a newer panorama loaded left a black viewer
  and a 'successful' open: the panorama now builds on the new canvas (or
  falls back to its front view).
- The streaming body read every real answer takes was tested with one
  chunk only: answers split mid-character are reassembled; the shared test
  fake now streams, so every source test runs the production path.

P3
- Lighting another source by its chip (params) now stops a running lookup
  and clears its spinner and error, as setProviderEnabled did; a restore
  that changes nothing leaves them alone.
- Panoramas decode off the main thread (ImageBitmap, RGBA upload: ~90 ms →
  ~15 ms at 4,096 px on a phone-class CPU) and are 2,048 px docked, 4,096
  once the viewer grows; a closed viewer keeps a 1×1 texture.
- The lit KARTAVIEW chip label is #8f8aff (~6:1; #635bff was 3.7:1).
- The viewer's keyboard focus ring is drawn above the photo.
- Tests: a second context loss and a drag on the new canvas; the look
  direction kept across a loss; a page's 30 s first try.
- The browser gate loses a real WebGL context in the 360° step and checks
  the 429 lookup was asked again (the fixture counter is reset first).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 00:36:58 +01:00
1 parent 97a1a667de
commit c99eb93174
12 files changed
+512 -26

No files matched your search

+55 -2
View File
@@ -42,7 +42,9 @@ import {
describeKartaViewCall,
KV_LINE,
KV_SEQUENCE_ID,
kartaViewLookups,
kartaViewPhotos,
resetKartaViewFixture,
} from './fixtures/street-level/kartaView.mjs';
const PANEL_ID = 'street-level-panel';
@@ -751,6 +753,8 @@ async function main() {
* draped map so a line is where it is drawn; leaves Mapillary lit.
*/
async function kartaViewSteps() {
// The first lookup from here on is the rate-limited one.
resetKartaViewFixture();
const ui = () =>
page.evaluate(() =>
window.__godsEyeView.dataManager.layers
@@ -895,6 +899,11 @@ async function main() {
const street = (await ui()).street;
if (!fixtures && /No street-level imagery/.test(street.error || ''))
return skip('no KartaView photo near the line');
if (fixtures)
assert.ok(
kartaViewLookups() >= 2,
`the rate-limited lookup was asked again (${kartaViewLookups()} lookups)`,
);
assert.equal(street.error, null, `viewer error: ${street.error}`);
assert.equal(street.providerId, 'kartaview');
assert.equal(street.renderModes, true, 'FIT / FILL for a flat photo');
@@ -964,7 +973,7 @@ async function main() {
);
if (fixtures)
await step(
'KartaView: a 360° photo opens on the panorama without FIT / FILL, and dragging turns it',
'KartaView: a 360° photo opens on the panorama without FIT / FILL, dragging turns it, and it survives a lost WebGL context',
async () => {
const pano = photos.find((photo) => photo.isPano);
await page.evaluate((id) => {
@@ -1004,12 +1013,56 @@ async function main() {
await uiUntil((v, was) => v.street.bearing !== was, bearing),
'dragging the panorama turned its bearing',
);
// A GPU reset: the panorama comes back on a new canvas, and drags.
assert.ok(
await page.evaluate(() => {
const canvas = document.querySelector('.sl-kv-pano');
window.__kvLostCanvas = canvas;
const lose = canvas
.getContext('webgl')
?.getExtension('WEBGL_lose_context');
lose?.loseContext();
return Boolean(lose);
}),
'the browser can lose a WebGL context on purpose',
);
assert.ok(
await settle(
page,
() => {
const canvas = document.querySelector('.sl-kv-pano');
return (
canvas &&
canvas !== window.__kvLostCanvas &&
!canvas.hidden &&
canvas.width > 1
);
},
null,
{ timeout: 30_000 },
),
'the panorama is drawn again on a new canvas',
);
const after = await ui();
assert.equal(after.street.imageId, pano.id);
assert.equal(after.street.isPano, true);
assert.equal(after.street.error, null);
const turned = after.street.bearing;
await page.mouse.move(box.x, box.y);
await page.mouse.down();
for (let i = 1; i <= 8; i++)
await page.mouse.move(box.x + i * 15, box.y);
await page.mouse.up();
assert.ok(
await uiUntil((v, was) => v.street.bearing !== was, turned),
'the new canvas drags',
);
},
);
else
await skipStep(
'fixtures only',
'KartaView: a 360° photo opens on the panorama without FIT / FILL, and dragging turns it',
'KartaView: a 360° photo opens on the panorama without FIT / FILL, dragging turns it, and it survives a lost WebGL context',
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers