From d7ebcf0ef8e0f7c25f7c61b9995ac05955362ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 1 May 2022 14:37:18 +0200 Subject: [PATCH 1/7] Fix error when image state changes on disposed renderer --- src/ol/renderer/Layer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 352466c8ce..f66c8c3a7d 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -196,7 +196,11 @@ class LayerRenderer extends Observable { */ renderIfReadyAndVisible() { const layer = this.getLayer(); - if (layer.getVisible() && layer.getSourceState() == SourceState.READY) { + if ( + layer && + layer.getVisible() && + layer.getSourceState() == SourceState.READY + ) { layer.changed(); } } From 5345b80aa1ac01ce196be1789fd11cbdcec6123b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 1 May 2022 14:39:10 +0200 Subject: [PATCH 2/7] Remove unused getImageState call --- src/ol/renderer/vector.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 86fcc2266c..9aa4f5ff0f 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -124,14 +124,13 @@ export function renderFeature( let loading = false; const imageStyle = style.getImage(); if (imageStyle) { - let imageState = imageStyle.getImageState(); + const imageState = imageStyle.getImageState(); if (imageState == ImageState.LOADED || imageState == ImageState.ERROR) { imageStyle.unlistenImageChange(listener); } else { if (imageState == ImageState.IDLE) { imageStyle.load(); } - imageState = imageStyle.getImageState(); imageStyle.listenImageChange(listener); loading = true; } From dc128820c11f4c28df9e132feb2ccbf575630d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 1 May 2022 16:56:11 +0200 Subject: [PATCH 3/7] Test rendercomplete waits for icon load --- test/browser/spec/ol/Map.test.js | 252 ++++++++++++++++++++----------- 1 file changed, 167 insertions(+), 85 deletions(-) diff --git a/test/browser/spec/ol/Map.test.js b/test/browser/spec/ol/Map.test.js index 79ed62456d..e87d41b6ba 100644 --- a/test/browser/spec/ol/Map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -25,6 +25,7 @@ import VectorSource from '../../../../src/ol/source/Vector.js'; import View from '../../../../src/ol/View.js'; import WebGLPointsLayer from '../../../../src/ol/layer/WebGLPoints.js'; import XYZ from '../../../../src/ol/source/XYZ.js'; +import {Icon, Style} from '../../../../src/ol/style.js'; import {LineString, Point, Polygon} from '../../../../src/ol/geom.js'; import {TRUE} from '../../../../src/ol/functions.js'; import { @@ -403,105 +404,186 @@ describe('ol/Map', function () { }); describe('rendercomplete event', function () { - let map; + let map, target; + beforeEach(function () { - const target = document.createElement('div'); + target = document.createElement('div'); target.style.width = '100px'; target.style.height = '100px'; document.body.appendChild(target); - map = new Map({ - target: target, - layers: [ - new TileLayer({ - opacity: 0.5, - source: new XYZ({ - url: 'spec/ol/data/osm-{z}-{x}-{y}.png', - }), - }), - new ImageLayer({ - source: new ImageStatic({ - url: 'spec/ol/data/osm-0-0-0.png', - imageExtent: getProjection('EPSG:3857').getExtent(), - projection: 'EPSG:3857', - }), - }), - new VectorLayer({ - source: new VectorSource({ - url: 'spec/ol/data/point.json', - format: new GeoJSON(), - }), - }), - new VectorLayer({ - source: new VectorSource({ - url: 'spec/ol/data/point.json', - format: new GeoJSON(), - strategy: tileStrategy(createXYZ()), - }), - }), - new VectorLayer({ - source: new VectorSource({ - features: [new Feature(new Point([0, 0]))], - }), - }), - new VectorLayer({ - source: new VectorSource({ - loader: function (extent, resolution, projection) { - this.addFeature(new Feature(new Point([0, 0]))); - }, - }), - }), - new WebGLPointsLayer({ - source: new VectorSource({ - features: [new Feature(new Point([0, 0]))], - }), - style: { - symbol: { - color: 'red', - symbolType: 'circle', - }, - }, - }), - ], - }); }); afterEach(function () { - document.body.removeChild(map.getTargetElement()); - map.setTarget(null); - map.dispose(); + disposeMap(map); map.getLayers().forEach((layer) => layer.dispose()); }); - it('triggers when all tiles and sources are loaded and faded in', function (done) { - const layers = map.getLayers().getArray(); - expect(layers[6].getRenderer().ready).to.be(false); - map.once('rendercomplete', function () { - expect(map.tileQueue_.getTilesLoading()).to.be(0); - expect(layers[1].getSource().image_.getState()).to.be( - ImageState.LOADED + describe('renderer ready property', function () { + beforeEach(function () { + map = new Map({ + target: target, + layers: [ + new TileLayer({ + opacity: 0.5, + source: new XYZ({ + url: 'spec/ol/data/osm-{z}-{x}-{y}.png', + }), + }), + new ImageLayer({ + source: new ImageStatic({ + url: 'spec/ol/data/osm-0-0-0.png', + imageExtent: getProjection('EPSG:3857').getExtent(), + projection: 'EPSG:3857', + }), + }), + new VectorLayer({ + source: new VectorSource({ + url: 'spec/ol/data/point.json', + format: new GeoJSON(), + }), + }), + new VectorLayer({ + source: new VectorSource({ + url: 'spec/ol/data/point.json', + format: new GeoJSON(), + strategy: tileStrategy(createXYZ()), + }), + }), + new VectorLayer({ + source: new VectorSource({ + features: [new Feature(new Point([0, 0]))], + }), + }), + new VectorLayer({ + source: new VectorSource({ + loader: function (extent, resolution, projection) { + this.addFeature(new Feature(new Point([0, 0]))); + }, + }), + }), + new WebGLPointsLayer({ + source: new VectorSource({ + features: [new Feature(new Point([0, 0]))], + }), + style: { + symbol: { + color: 'red', + symbolType: 'circle', + }, + }, + }), + ], + }); + }); + + it('triggers when all tiles and sources are loaded and faded in', function (done) { + const layers = map.getLayers().getArray(); + expect(layers[6].getRenderer().ready).to.be(false); + map.once('rendercomplete', function () { + expect(map.tileQueue_.getTilesLoading()).to.be(0); + expect(layers[1].getSource().image_.getState()).to.be( + ImageState.LOADED + ); + expect(layers[2].getSource().getFeatures().length).to.be(1); + expect(layers[6].getRenderer().ready).to.be(true); + done(); + }); + map.setView( + new View({ + center: [0, 0], + zoom: 0, + }) ); - expect(layers[2].getSource().getFeatures().length).to.be(1); - expect(layers[6].getRenderer().ready).to.be(true); - done(); }); - map.setView( - new View({ - center: [0, 0], - zoom: 0, - }) - ); + + it('ignores invisible layers', function (done) { + map.getLayers().forEach((layer, i) => layer.setVisible(i === 4)); + map.setView( + new View({ + center: [0, 0], + zoom: 0, + }) + ); + map.once('rendercomplete', () => done()); + }); }); - it('ignores invisible layers', function (done) { - map.getLayers().forEach(function (layer, i) { - layer.setVisible(i === 4); + + describe('icon loading', function () { + /** @type {Icon} */ + let icon; + beforeEach(function () { + icon = new Icon({ + src: 'spec/ol/data/dot.png', + }); + + const delay = 100; + // Delay icon change events + let states = [{state: icon.getImageState()}]; + icon.listenImageChange = function (listener) { + if (!listener._delay) { + listener._delay = (e) => { + const key = setTimeout(() => { + states.shift(); + listener.call(this, e); + }, delay); + Object.assign(states[states.length - 1], {key, listener}); + states.push({ + state: Icon.prototype.getImageState.call(this), + }); + }; + } + return Icon.prototype.listenImageChange.call(this, listener._delay); + }; + icon.unlistenImageChange = function (listener) { + states = states.filter((state) => { + if (state.listener !== listener) { + return true; + } + clearTimeout(listener.key); + return false; + }); + const addedListener = listener._delay; + delete listener._delay; + return Icon.prototype.unlistenImageChange.call(this, addedListener); + }; + icon.getImageState = function () { + return states[0].state; + }; + }); + + it('waits for icons to load', function (done) { + map = new Map({ + target: target, + view: new View({ + center: [0, 0], + resolution: 1, + }), + layers: [ + new VectorLayer({ + source: new VectorSource({ + features: [new Feature(new Point([0, 0]))], + }), + style: new Style({ + image: icon, + }), + }), + ], + }); + let loaded = false; + icon.listenImageChange(function (e) { + if (e.target.getImageState() === ImageState.LOADED) { + loaded = true; + } + }); + map.once('rendercomplete', function () { + try { + expect(loaded).to.be(true); + done(); + } catch (e) { + done(e); + } + }); }); - map.setView( - new View({ - center: [0, 0], - zoom: 0, - }) - ); - map.once('rendercomplete', () => done()); }); }); From de392a98465ef0de5b03909cae175af9e21d2ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 1 May 2022 14:36:03 +0200 Subject: [PATCH 4/7] Don't fire rendercomplete event while icons are loading --- src/ol/renderer/canvas/VectorLayer.js | 17 +++++------------ src/ol/renderer/canvas/VectorTileLayer.js | 8 +------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index c83441b5f3..1e495ed189 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -62,12 +62,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { */ this.animatingOrInteracting_; - /** - * @private - * @type {boolean} - */ - this.dirty_ = false; - /** * @type {ImageData} */ @@ -523,7 +517,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); if ( - (!this.dirty_ && !updateWhileAnimating && animating) || + (this.ready && !updateWhileAnimating && animating) || (!updateWhileInteracting && interacting) ) { this.animatingOrInteracting_ = true; @@ -594,7 +588,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } if ( - !this.dirty_ && + this.ready && this.renderedResolution_ == resolution && this.renderedRevision_ == vectorLayerRevision && this.renderedRenderOrder_ == vectorLayerRenderOrder && @@ -611,8 +605,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.replayGroup_ = null; - this.dirty_ = false; - const replayGroup = new CanvasBuilderGroup( getRenderTolerance(resolution, pixelRatio), extent, @@ -650,7 +642,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); - + let ready = true; const render = /** * @param {import("../../Feature.js").default} feature Feature. @@ -672,7 +664,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { userTransform, declutterBuilderGroup ); - this.dirty_ = this.dirty_ || dirty; + ready = ready && !dirty; } }.bind(this); @@ -686,6 +678,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { render(features[i]); } this.renderedFeatures_ = features; + this.ready = ready; const replayGroupInstructions = replayGroup.finish(); const executorGroup = new ExecutorGroup( diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 7f18341d5f..9b2463602d 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -85,12 +85,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** @private */ this.boundHandleStyleImageChange_ = this.handleStyleImageChange_.bind(this); - /** - * @private - * @type {boolean} - */ - this.dirty_ = false; - /** * @private * @type {number} @@ -289,7 +283,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { builderGroup, declutterBuilderGroup ); - this.dirty_ = this.dirty_ || dirty; builderState.dirty = builderState.dirty || dirty; } }; @@ -339,6 +332,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { builderState.renderedRevision = revision; builderState.renderedRenderOrder = renderOrder; builderState.renderedResolution = resolution; + this.ready = !builderState.dirty; } /** From 2473e5bd323d556b2e75d600750d66c41a52d6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 17 May 2022 02:06:55 +0200 Subject: [PATCH 5/7] Fix vector tile renderer ready --- src/ol/renderer/canvas/VectorTileLayer.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 9b2463602d..aa604c1230 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -188,10 +188,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ prepareFrame(frameState) { const layerRevision = this.getLayer().getRevision(); - if (this.renderedLayerRevision_ != layerRevision) { + if (this.renderedLayerRevision_ !== layerRevision) { + this.renderedLayerRevision_ = layerRevision; this.renderedTiles.length = 0; } - this.renderedLayerRevision_ = layerRevision; return super.prepareFrame(frameState); } @@ -232,6 +232,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (declutter) { tile.declutterExecutorGroups[layerUid] = []; } + builderState.dirty = false; for (let t = 0, tt = sourceTiles.length; t < tt; ++t) { const sourceTile = sourceTiles[t]; if (sourceTile.getState() != TileState.LOADED) { @@ -249,7 +250,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const bufferedExtent = equals(sourceTileExtent, sharedExtent) ? null : builderExtent; - builderState.dirty = false; const builderGroup = new CanvasBuilderGroup( 0, builderExtent, @@ -332,7 +332,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { builderState.renderedRevision = revision; builderState.renderedRenderOrder = renderOrder; builderState.renderedResolution = resolution; - this.ready = !builderState.dirty; } /** @@ -667,10 +666,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const tiles = this.renderedTiles; const clips = []; const clipZs = []; + let ready = true; for (let i = tiles.length - 1; i >= 0; --i) { const tile = /** @type {import("../../VectorRenderTile.js").default} */ ( tiles[i] ); + ready = ready && !tile.getReplayState(layer).dirty; const executorGroups = tile.executorGroups[getUid(layer)].filter( (group) => group.hasExecutors(replayTypes) ); @@ -735,6 +736,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } context.globalAlpha = alpha; + this.ready = ready; return this.container; } From 11589f3317c9230065c19538947a92fbce8d03ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 24 May 2022 22:18:36 +0200 Subject: [PATCH 6/7] Test rendercomplete with icon for VectorTile --- test/browser/spec/ol/Map.test.js | 68 +++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/test/browser/spec/ol/Map.test.js b/test/browser/spec/ol/Map.test.js index e87d41b6ba..db2f1d5d81 100644 --- a/test/browser/spec/ol/Map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -22,6 +22,8 @@ import TileLayer from '../../../../src/ol/layer/Tile.js'; import TileLayerRenderer from '../../../../src/ol/renderer/canvas/TileLayer.js'; import VectorLayer from '../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; +import VectorTileLayer from '../../../../src/ol/layer/VectorTile.js'; +import VectorTileSource from '../../../../src/ol/source/VectorTile.js'; import View from '../../../../src/ol/View.js'; import WebGLPointsLayer from '../../../../src/ol/layer/WebGLPoints.js'; import XYZ from '../../../../src/ol/source/XYZ.js'; @@ -37,6 +39,7 @@ import { } from '../../../../src/ol/proj.js'; import {createXYZ} from '../../../../src/ol/tilegrid.js'; import {defaults as defaultInteractions} from '../../../../src/ol/interaction.js'; +import {shared as iconImageCache} from '../../../../src/ol/style/IconImageCache.js'; import {tile as tileStrategy} from '../../../../src/ol/loadingstrategy.js'; describe('ol/Map', function () { @@ -508,12 +511,13 @@ describe('ol/Map', function () { }); }); - describe('icon loading', function () { + describe('with icons', function () { /** @type {Icon} */ let icon; beforeEach(function () { + iconImageCache.clear(); icon = new Icon({ - src: 'spec/ol/data/dot.png', + src: 'spec/ol/data/dot.png?delayed', }); const delay = 100; @@ -551,7 +555,59 @@ describe('ol/Map', function () { }; }); - it('waits for icons to load', function (done) { + it('waits for icons to be loaded with ol/renderer/canvas/VectorTileLayer', function (done) { + const delayIconAtTile = 1; + let tilesRequested = 0; + const tileSize = 64; + const tileGrid = createXYZ({tileSize: tileSize}); + map = new Map({ + target: target, + view: new View({ + center: [0, 0], + resolution: 1, + }), + layers: [ + new VectorTileLayer({ + source: new VectorTileSource({ + tileSize: tileSize, + tileUrlFunction: (tileCoord) => tileCoord.join('/'), + tileLoadFunction: function (tile, url) { + const coordinate = tileGrid.getTileCoordCenter( + tile.getTileCoord() + ); + const feature = new Feature(new Point(coordinate)); + tile.setFeatures([feature]); + if (tilesRequested++ === delayIconAtTile) { + feature.setStyle(new Style({image: icon})); + } + }, + }), + style: new Style({ + image: new Icon({ + src: 'spec/ol/data/dot.png', + }), + }), + }), + ], + }); + let iconLoaded = false; + icon.listenImageChange(function (e) { + if (e.target.getImageState() === ImageState.LOADED) { + iconLoaded = true; + } + }); + map.once('rendercomplete', function () { + try { + expect(tilesRequested).to.be.greaterThan(delayIconAtTile); + expect(iconLoaded).to.be(true); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('waits for icons to be loaded with ol/renderer/canvas/VectorLayer', function (done) { map = new Map({ target: target, view: new View({ @@ -569,15 +625,15 @@ describe('ol/Map', function () { }), ], }); - let loaded = false; + let iconLoaded = false; icon.listenImageChange(function (e) { if (e.target.getImageState() === ImageState.LOADED) { - loaded = true; + iconLoaded = true; } }); map.once('rendercomplete', function () { try { - expect(loaded).to.be(true); + expect(iconLoaded).to.be(true); done(); } catch (e) { done(e); From e8925a90e5b66f467e58c0a1a0933d8252144d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 24 May 2022 22:20:40 +0200 Subject: [PATCH 7/7] Test VectorTile layer not ready while loading needed icons --- test/browser/spec/ol/layer/vectortile.test.js | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/test/browser/spec/ol/layer/vectortile.test.js b/test/browser/spec/ol/layer/vectortile.test.js index 5f839a4e03..c4da7dd372 100644 --- a/test/browser/spec/ol/layer/vectortile.test.js +++ b/test/browser/spec/ol/layer/vectortile.test.js @@ -1,9 +1,17 @@ +import Feature from '../../../../../src/ol/Feature.js'; import GeoJSON from '../../../../../src/ol/format/GeoJSON.js'; +import ImageState from '../../../../../src/ol/ImageState.js'; import Map from '../../../../../src/ol/Map.js'; import VectorTileLayer from '../../../../../src/ol/layer/VectorTile.js'; import VectorTileSource from '../../../../../src/ol/source/VectorTile.js'; import View from '../../../../../src/ol/View.js'; -import {fromLonLat} from '../../../../../src/ol/proj.js'; +import {Icon, Style} from '../../../../../src/ol/style.js'; +import {Point} from '../../../../../src/ol/geom.js'; +import {create as createTransform} from '../../../../../src/ol/transform.js'; +import {createXYZ} from '../../../../../src/ol/tilegrid.js'; +import {fromLonLat, get as getProjection} from '../../../../../src/ol/proj.js'; +import {getUid} from '../../../../../src/ol/util.js'; +import {isEmpty} from '../../../../../src/ol/obj.js'; describe('ol.layer.VectorTile', function () { describe('constructor (defaults)', function () { @@ -150,4 +158,94 @@ describe('ol.layer.VectorTile', function () { }); }); }); + + describe('#renderFrame', function () { + /** @type {VectorTileLayer} */ let layer; + + afterEach(function () { + layer.dispose(); + }); + + it('sets ready property to false when icons are loading', function (done) { + const zoom = 1; + const tileSize = 32; + const projection = getProjection('EPSG:3857'); + const tileGrid = createXYZ({tileSize: tileSize}); + const resolution = tileGrid.getResolution(zoom); + layer = new VectorTileLayer({ + renderBuffer: 0, + source: new VectorTileSource({ + tileSize: tileSize, + tileUrlFunction: (tileCoord) => tileCoord.join('/'), + tileLoadFunction: function (tile, url) { + const coordinate = tileGrid.getTileCoordCenter(tile.getTileCoord()); + tile.setFeatures([new Feature(new Point(coordinate))]); + }, + }), + style: new Style({ + image: new Icon({ + src: + 'data:image/svg+xml;base64,' + + window.btoa(` + + `), + }), + }), + }); + const renderer = layer.getRenderer(); + const frameState = + /** @type {import("../../../../../src/ol/PluggableMap.js").FrameState} */ ({ + pixelRatio: 1, + viewState: { + zoom: zoom, + resolution: resolution, + center: [0, 0], + rotation: 0, + projection: projection, + }, + size: [2 * tileSize, 2 * tileSize], + extent: [-tileSize, -tileSize, tileSize, tileSize].map( + (n) => n * resolution + ), + viewHints: [0, 0], + layerStatesArray: layer.getLayerStatesArray(), + layerIndex: 0, + wantedTiles: {}, + usedTiles: {}, + tileQueue: {isKeyQueued: () => true}, + pixelToCoordinateTransform: createTransform(), + }); + + renderer.renderFrame(frameState); + // Tiles not yet loaded, no icon queued + expect(renderer.ready).to.be(true); + const source = layer.getSource(); + const wantedTiles = frameState.wantedTiles[getUid(source)]; + expect(isEmpty(wantedTiles)).to.be(false); + + // Tiles are loaded synchronously + source.tileCache.forEach((tile) => tile.load()); + + renderer.renderFrame(frameState); + // Tiles loaded, waiting for icon + expect(renderer.ready).to.be(false); + + layer + .getStyle() + .getImage() + .listenImageChange(function (evt) { + if (evt.target.getImageState() !== ImageState.LOADED) { + return; + } + try { + renderer.renderFrame(frameState); + // Tiles and icon loaded + expect(renderer.ready).to.be(true); + done(); + } catch (e) { + done(e); + } + }); + }); + }); });