2017-12-06 17:20:39 -08:00
/* global QUnit, testLog */
2014-11-20 11:51:24 -08:00
( function ( ) {
2023-11-26 21:32:26 +01:00
const Convertor = OpenSeadragon . convertor ,
T _A = "__TEST__typeA" , T _B = "__TEST__typeB" , T _C = "__TEST__typeC" , T _D = "__TEST__typeD" , T _E = "__TEST__typeE" ;
2023-11-18 20:16:35 +01:00
let viewer ;
//we override jobs: remember original function
const originalJob = OpenSeadragon . ImageLoader . prototype . addJob ;
//event awaiting
function waitFor ( predicate ) {
const time = setInterval ( ( ) => {
if ( predicate ( ) ) {
clearInterval ( time ) ;
}
} , 20 ) ;
}
2024-11-21 15:35:27 +01:00
const sleep = ( ms ) => new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
// other tests will interfere
let typeAtoB = 0 , typeBtoC = 0 , typeCtoA = 0 , typeDtoA = 0 , typeCtoE = 0 ;
//set all same costs to get easy testing, know which path will be taken
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _A , T _B , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
typeAtoB ++ ;
return x + 1 ;
} ) ;
2024-11-21 15:35:27 +01:00
// Costly conversion to C simulation
Convertor . learn ( T _B , T _C , async ( tile , x ) => {
2023-11-26 21:32:26 +01:00
typeBtoC ++ ;
2024-11-21 15:35:27 +01:00
await sleep ( 5 ) ;
2023-11-26 21:32:26 +01:00
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _C , T _A , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
typeCtoA ++ ;
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _D , T _A , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
typeDtoA ++ ;
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _C , T _E , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
typeCtoE ++ ;
return x + 1 ;
} ) ;
//'Copy constructors'
let copyA = 0 , copyB = 0 , copyC = 0 , copyD = 0 , copyE = 0 ;
//also learn destructors
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _A , T _A , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
copyA ++ ;
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _B , T _B , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
copyB ++ ;
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _C , T _C , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
copyC ++ ;
return x - 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _D , T _D , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
copyD ++ ;
return x + 1 ;
} ) ;
2024-02-04 18:48:25 +01:00
Convertor . learn ( T _E , T _E , ( tile , x ) => {
2023-11-26 21:32:26 +01:00
copyE ++ ;
return x + 1 ;
} ) ;
let destroyA = 0 , destroyB = 0 , destroyC = 0 , destroyD = 0 , destroyE = 0 ;
//also learn destructors
Convertor . learnDestroy ( T _A , ( ) => {
destroyA ++ ;
} ) ;
Convertor . learnDestroy ( T _B , ( ) => {
destroyB ++ ;
} ) ;
Convertor . learnDestroy ( T _C , ( ) => {
destroyC ++ ;
} ) ;
Convertor . learnDestroy ( T _D , ( ) => {
destroyD ++ ;
} ) ;
Convertor . learnDestroy ( T _E , ( ) => {
destroyE ++ ;
} ) ;
2024-11-21 15:35:27 +01:00
OpenSeadragon . TestCacheDrawer = class extends OpenSeadragon . DrawerBase {
constructor ( opts ) {
super ( opts ) ;
this . testEvents = new OpenSeadragon . EventSource ( ) ;
}
static isSupported ( ) {
return true ;
}
_createDrawingElement ( ) {
return document . createElement ( "div" ) ;
}
draw ( tiledImages ) {
for ( let image of tiledImages ) {
const tilesDoDraw = image . getTilesToDraw ( ) . map ( info => info . tile ) ;
for ( let tile of tilesDoDraw ) {
const data = this . getDataToDraw ( tile ) ;
this . testEvents . raiseEvent ( 'test-tile' , {
tile : tile ,
dataToDraw : data ,
} ) ;
}
}
}
2025-01-10 22:05:16 +01:00
dataFree ( data ) {
this . testEvents . raiseEvent ( 'free-data' ) ;
}
2024-11-21 15:35:27 +01:00
canRotate ( ) {
return true ;
}
destroy ( ) {
2025-01-10 22:05:16 +01:00
this . destroyInternalCache ( ) ;
2024-11-21 15:35:27 +01:00
}
setImageSmoothingEnabled ( imageSmoothingEnabled ) {
//noop
}
drawDebuggingRect ( rect ) {
//noop
}
clear ( ) {
//noop
}
}
2025-01-10 22:05:16 +01:00
OpenSeadragon . SyncInternalCacheDrawer = class extends OpenSeadragon . TestCacheDrawer {
getType ( ) {
return "test-cache-drawer-sync" ;
}
getSupportedDataFormats ( ) {
return [ T _C , T _E ] ;
}
// Make test use private cache
get defaultOptions ( ) {
return {
usePrivateCache : true ,
preloadCache : false ,
} ;
}
dataCreate ( cache , tile ) {
this . testEvents . raiseEvent ( 'create-data' ) ;
return cache . data ;
}
}
OpenSeadragon . AsnycInternalCacheDrawer = class extends OpenSeadragon . TestCacheDrawer {
getType ( ) {
return "test-cache-drawer-async" ;
}
getSupportedDataFormats ( ) {
return [ T _A ] ;
}
// Make test use private cache
get defaultOptions ( ) {
return {
usePrivateCache : true ,
preloadCache : true ,
} ;
}
dataCreate ( cache , tile ) {
this . testEvents . raiseEvent ( 'create-data' ) ;
return cache . getDataAs ( T _C , true ) ;
}
dataFree ( data ) {
super . dataFree ( data ) ;
// Be nice and truly destroy the data copy
OpenSeadragon . convertor . destroy ( data , T _C ) ;
}
}
2024-11-21 15:35:27 +01:00
OpenSeadragon . EmptyTestT _ATileSource = class extends OpenSeadragon . TileSource {
supports ( data , url ) {
return data && data . isTestSource ;
}
configure ( data , url , postData ) {
return {
width : 512 , /* width *required */
height : 512 , /* height *required */
tileSize : 128 , /* tileSize *required */
tileOverlap : 0 , /* tileOverlap *required */
minLevel : 0 , /* minLevel */
maxLevel : 3 , /* maxLevel */
tilesUrl : "" , /* tilesUrl */
fileFormat : "" , /* fileFormat */
displayRects : null /* displayRects */
}
}
getTileUrl ( level , x , y ) {
return String ( level ) ; //treat each tile on level same to introduce cache overlaps
}
downloadTileStart ( context ) {
context . finish ( 0 , null , T _A ) ;
}
}
2014-11-20 11:51:24 -08:00
// ----------
2017-12-06 17:20:39 -08:00
QUnit . module ( 'TileCache' , {
beforeEach : function ( ) {
2023-11-18 20:16:35 +01:00
$ ( '<div id="example"></div>' ) . appendTo ( "#qunit-fixture" ) ;
2014-11-20 11:51:24 -08:00
testLog . reset ( ) ;
2023-11-18 20:16:35 +01:00
OpenSeadragon . ImageLoader . prototype . addJob = originalJob ;
2024-10-18 14:38:04 +02:00
// Reset counters
typeAtoB = 0 , typeBtoC = 0 , typeCtoA = 0 , typeDtoA = 0 , typeCtoE = 0 ;
copyA = 0 , copyB = 0 , copyC = 0 , copyD = 0 , copyE = 0 ;
destroyA = 0 , destroyB = 0 , destroyC = 0 , destroyD = 0 , destroyE = 0 ;
2014-11-20 11:51:24 -08:00
} ,
2017-12-06 17:20:39 -08:00
afterEach : function ( ) {
2023-11-18 20:16:35 +01:00
if ( viewer && viewer . close ) {
viewer . close ( ) ;
}
viewer = null ;
2014-11-20 11:51:24 -08:00
}
} ) ;
// ----------
2017-12-06 17:20:39 -08:00
QUnit . test ( 'basics' , function ( assert ) {
2023-11-26 21:32:26 +01:00
const done = assert . async ( ) ;
2024-03-05 10:48:07 +01:00
const fakeViewer = MockSeadragon . getViewer (
MockSeadragon . getDrawer ( {
2024-02-05 09:42:26 +01:00
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats ( ) {
return [ T _A , T _B , T _C , T _D , T _E ] ;
}
2024-03-05 10:48:07 +01:00
} )
) ;
const fakeTiledImage0 = MockSeadragon . getTiledImage ( fakeViewer ) ;
const fakeTiledImage1 = MockSeadragon . getTiledImage ( fakeViewer ) ;
2014-11-20 11:51:24 -08:00
2024-03-05 10:48:07 +01:00
const tile0 = MockSeadragon . getTile ( 'foo.jpg' , fakeTiledImage0 ) ;
const tile1 = MockSeadragon . getTile ( 'foo.jpg' , fakeTiledImage1 ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
const cache = new OpenSeadragon . TileCache ( ) ;
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 0 , 'no tiles to begin with' ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
tile0 . _caches [ tile0 . cacheKey ] = cache . cacheTile ( {
tile : tile0 ,
2024-10-18 14:38:04 +02:00
tiledImage : fakeTiledImage0 ,
data : 3 ,
dataType : T _A
2014-11-20 11:51:24 -08:00
} ) ;
2023-11-26 21:32:26 +01:00
tile0 . _cacheSize ++ ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 1 , 'tile count after cache' ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
tile1 . _caches [ tile1 . cacheKey ] = cache . cacheTile ( {
tile : tile1 ,
2024-10-18 14:38:04 +02:00
tiledImage : fakeTiledImage1 ,
data : 55 ,
dataType : T _B
2014-11-20 11:51:24 -08:00
} ) ;
2023-11-26 21:32:26 +01:00
tile1 . _cacheSize ++ ;
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 2 , 'tile count after second cache' ) ;
2014-11-20 11:51:24 -08:00
cache . clearTilesFor ( fakeTiledImage0 ) ;
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 1 , 'tile count after first clear' ) ;
2014-11-20 11:51:24 -08:00
cache . clearTilesFor ( fakeTiledImage1 ) ;
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 0 , 'tile count after second clear' ) ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
done ( ) ;
2014-11-20 11:51:24 -08:00
} ) ;
// ----------
2017-12-06 17:20:39 -08:00
QUnit . test ( 'maxImageCacheCount' , function ( assert ) {
2023-11-26 21:32:26 +01:00
const done = assert . async ( ) ;
2024-03-05 10:48:07 +01:00
const fakeViewer = MockSeadragon . getViewer (
MockSeadragon . getDrawer ( {
2024-02-05 09:42:26 +01:00
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats ( ) {
return [ T _A , T _B , T _C , T _D , T _E ] ;
}
2024-03-05 10:48:07 +01:00
} )
) ;
const fakeTiledImage0 = MockSeadragon . getTiledImage ( fakeViewer ) ;
const tile0 = MockSeadragon . getTile ( 'different.jpg' , fakeTiledImage0 ) ;
const tile1 = MockSeadragon . getTile ( 'same.jpg' , fakeTiledImage0 ) ;
const tile2 = MockSeadragon . getTile ( 'same.jpg' , fakeTiledImage0 ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
const cache = new OpenSeadragon . TileCache ( {
2014-11-20 11:51:24 -08:00
maxImageCacheCount : 1
} ) ;
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 0 , 'no tiles to begin with' ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
tile0 . _caches [ tile0 . cacheKey ] = cache . cacheTile ( {
tile : tile0 ,
2024-10-18 14:38:04 +02:00
tiledImage : fakeTiledImage0 ,
data : 55 ,
dataType : T _B
2014-11-20 11:51:24 -08:00
} ) ;
2023-11-26 21:32:26 +01:00
tile0 . _cacheSize ++ ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 1 , 'tile count after add' ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
tile1 . _caches [ tile1 . cacheKey ] = cache . cacheTile ( {
tile : tile1 ,
2024-10-18 14:38:04 +02:00
tiledImage : fakeTiledImage0 ,
data : 55 ,
dataType : T _B
2014-11-20 11:51:24 -08:00
} ) ;
2023-11-26 21:32:26 +01:00
tile1 . _cacheSize ++ ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 1 , 'tile count after add of second image' ) ;
2014-11-20 11:51:24 -08:00
2023-11-26 21:32:26 +01:00
tile2 . _caches [ tile2 . cacheKey ] = cache . cacheTile ( {
tile : tile2 ,
2024-10-18 14:38:04 +02:00
tiledImage : fakeTiledImage0 ,
data : 55 ,
dataType : T _B
2014-11-20 11:51:24 -08:00
} ) ;
2023-11-26 21:32:26 +01:00
tile2 . _cacheSize ++ ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
assert . equal ( cache . numTilesLoaded ( ) , 2 , 'tile count after additional same image' ) ;
2014-11-20 11:51:24 -08:00
2017-12-06 17:20:39 -08:00
done ( ) ;
2014-11-20 11:51:24 -08:00
} ) ;
2025-01-10 22:05:16 +01:00
// Tile API and cache interaction
QUnit . test ( 'Tile: basic rendering & test setup (sync drawer)' , function ( test ) {
const done = test . async ( ) ;
viewer = OpenSeadragon ( {
id : 'example' ,
prefixUrl : '/build/openseadragon/images/' ,
maxImageCacheCount : 200 , //should be enough to fit test inside the cache
springStiffness : 100 , // Faster animation = faster tests
drawer : 'test-cache-drawer-sync' ,
} ) ;
const tileCache = viewer . tileCache ;
const drawer = viewer . drawer ;
let testTileCalled = false ;
let countFreeCalled = 0 ;
let countCreateCalled = 0 ;
drawer . testEvents . addHandler ( 'test-tile' , e => {
testTileCalled = true ;
test . ok ( e . dataToDraw , "Tile data is ready to be drawn" ) ;
} ) ;
drawer . testEvents . addHandler ( 'create-data' , e => {
countCreateCalled ++ ;
} ) ;
drawer . testEvents . addHandler ( 'free-data' , e => {
countFreeCalled ++ ;
} ) ;
viewer . addHandler ( 'open' , async ( ) => {
await viewer . waitForFinishedJobsForTest ( ) ;
await sleep ( 1 ) ; // necessary to make space for a draw call
test . ok ( viewer . world . getItemAt ( 0 ) . source instanceof OpenSeadragon . EmptyTestT _ATileSource , "Tests are done with empty test source type T_A." ) ;
test . ok ( viewer . world . getItemAt ( 1 ) . source instanceof OpenSeadragon . EmptyTestT _ATileSource , "Tests are done with empty test source type T_A." ) ;
test . ok ( testTileCalled , "Drawer tested at least one tile." ) ;
test . ok ( typeAtoB > 1 , "At least one conversion was triggered." ) ;
test . equal ( typeAtoB , typeBtoC , "A->B = B->C, since we need to move all data to T_C for the drawer." ) ;
for ( let tile of tileCache . _tilesLoaded ) {
const cache = tile . getCache ( ) ;
test . equal ( cache . type , T _C , "Cache data was affected, the drawer supports only T_C since there is no way to get to T_E." ) ;
const internalCache = cache . getDataForRendering ( drawer , tile ) ;
test . equal ( internalCache . type , viewer . drawer . getId ( ) , "Sync conversion routine means T_C is also internal since dataCreate only creates data. However, internal cache keeps type of the drawer ID." ) ;
test . ok ( internalCache . loaded , "Internal cache ready." ) ;
}
test . ok ( countCreateCalled > 0 , "Internal cache creation called." ) ;
viewer . drawer . destroyInternalCache ( ) ;
test . equal ( countCreateCalled , countFreeCalled , "Free called as many times as create." ) ;
done ( ) ;
} ) ;
viewer . open ( [
{ isTestSource : true } ,
{ isTestSource : true } ,
] ) ;
} ) ;
QUnit . test ( 'Tile & Invalidation API: basic conversion & preprocessing' , function ( test ) {
const done = test . async ( ) ;
viewer = OpenSeadragon ( {
id : 'example' ,
prefixUrl : '/build/openseadragon/images/' ,
maxImageCacheCount : 200 , //should be enough to fit test inside the cache
springStiffness : 100 , // Faster animation = faster tests
drawer : 'test-cache-drawer-async' ,
} ) ;
const tileCache = viewer . tileCache ;
const drawer = viewer . drawer ;
let testTileCalled = false ;
let _currentTestVal = undefined ;
let previousTestValue = undefined ;
drawer . testEvents . addHandler ( 'test-tile' , e => {
test . ok ( e . dataToDraw , "Tile data is ready to be drawn" ) ;
if ( _currentTestVal !== undefined ) {
testTileCalled = true ;
test . equal ( e . dataToDraw , _currentTestVal , "Value is correct on the drawn data." ) ;
}
} ) ;
function testDrawingRoutine ( value ) {
_currentTestVal = value ;
viewer . world . needsDraw ( ) ;
viewer . world . draw ( ) ;
_currentTestVal = undefined ;
}
viewer . addHandler ( 'open' , async ( ) => {
await viewer . waitForFinishedJobsForTest ( ) ;
await sleep ( 1 ) ; // necessary to make space for a draw call
// Test simple data set -> creates main cache
let testHandler = async e => {
// data comes in as T_A
test . equal ( typeDtoA , 0 , "No conversion needed to get type A." ) ;
test . equal ( typeCtoA , 0 , "No conversion needed to get type A." ) ;
const data = await e . getData ( T _A ) ;
test . equal ( data , 1 , "Copy: creation of a working cache." ) ;
e . tile . _ _TEST _PROCESSED = true ;
// Test value 2 since we set T_C no need to convert
await e . setData ( 2 , T _C ) ;
test . notOk ( e . outdated ( ) , "Event is still valid." ) ;
} ;
viewer . addHandler ( 'tile-invalidated' , testHandler ) ;
await viewer . world . requestInvalidate ( true ) ;
//test for each level only single cache was processed
const processedLevels = { } ;
for ( let tile of tileCache . _tilesLoaded ) {
const level = tile . level ;
if ( tile . _ _TEST _PROCESSED ) {
test . ok ( ! processedLevels [ level ] , "Only single tile processed per level." ) ;
processedLevels [ level ] = true ;
delete tile . _ _TEST _PROCESSED ;
}
const origCache = tile . getCache ( tile . originalCacheKey ) ;
test . equal ( origCache . type , T _A , "Original cache data was not affected, the drawer uses internal cache." ) ;
test . equal ( origCache . data , 0 , "Original cache data was not affected, the drawer uses internal cache." ) ;
const cache = tile . getCache ( ) ;
test . equal ( cache . type , T _A , "Main Cache Converted T_C -> T_A (drawer supports type A) (suite 1)" ) ;
test . equal ( cache . data , 3 , "Conversion step increases plugin-stored value 2 to 3" ) ;
const internalCache = cache . getDataForRendering ( drawer , tile ) ;
test . equal ( internalCache . type , viewer . drawer . getId ( ) , "Internal cache has type of the drawer ID." ) ;
test . ok ( internalCache . loaded , "Internal cache ready." ) ;
}
// Internal cache will have value 5: main cache is 3, type is T_A,
testDrawingRoutine ( 5 ) ; // internal cache transforms to T_C: two steps, TA->TB->TC 3+2
// Test that basic scenario with reset data false starts from the main cache data of previous round
const modificationConstant = 50 ;
viewer . removeHandler ( 'tile-invalidated' , testHandler ) ;
testHandler = async e => {
const data = await e . getData ( T _B ) ;
test . equal ( data , 4 , "A -> B conversion happened, we started from value 3 in the main cache." ) ;
await e . setData ( data + modificationConstant , T _B ) ;
test . notOk ( e . outdated ( ) , "Event is still valid." ) ;
} ;
viewer . addHandler ( 'tile-invalidated' , testHandler ) ;
await viewer . world . requestInvalidate ( false ) ;
// We set data as TB - there is required T_A: T_B -> T_C -> T_A conversion round on the main cache
let newValue = modificationConstant + 4 + 2 ;
// and there is still requirement of T_C on internal data, +2 steps
testDrawingRoutine ( newValue + 2 ) ;
for ( let tile of tileCache . _tilesLoaded ) {
const cache = tile . getCache ( ) ;
test . equal ( cache . type , T _A , "Main Cache Updated (suite 2)." ) ;
test . equal ( cache . data , newValue , "Main Cache Updated (suite 2)." ) ;
}
// Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration
viewer . removeHandler ( 'tile-invalidated' , testHandler ) ;
testHandler = async e => {
const data = await e . getData ( T _B ) ;
test . equal ( data , 1 , "Copy: creation of a working cache." ) ;
await e . setData ( - 8 , T _E ) ;
e . resetData ( ) ;
} ;
viewer . addHandler ( 'tile-invalidated' , testHandler ) ;
await viewer . world . requestInvalidate ( true ) ;
await sleep ( 1 ) ; // necessary to make space for a draw call
testDrawingRoutine ( 2 ) ; // Value +2 rendering from original data
for ( let tile of tileCache . _tilesLoaded ) {
const origCache = tile . getCache ( tile . originalCacheKey ) ;
test . ok ( tile . getCache ( ) === origCache , "Main cache is now original cache." ) ;
}
// Now force main cache creation that differs
viewer . removeHandler ( 'tile-invalidated' , testHandler ) ;
testHandler = async e => {
await e . setData ( 41 , T _B ) ;
} ;
viewer . addHandler ( 'tile-invalidated' , testHandler ) ;
await viewer . world . requestInvalidate ( true ) ;
// Now test whether data reset works, even with non-original data
viewer . removeHandler ( 'tile-invalidated' , testHandler ) ;
testHandler = async e => {
const data = await e . getData ( T _B ) ;
test . equal ( data , 44 , "Copy: 41 +2 (previous request invalidate ends at T_A) + 1 (we request type B)." ) ;
await e . setData ( data , T _E ) ; // there is no way to convert T_E -> T_A, this would throw an error
e . resetData ( ) ; // reset data will revert to original cache
} ;
viewer . addHandler ( 'tile-invalidated' , testHandler ) ;
// The data will be 45 since no change has been made:
// last main cache set was 41 T_B, supported T_A = +2
// and internal requirement T_C = +2
const checkNotCalled = e => {
test . ok ( false , "Create data must not be called when there is no change!" ) ;
} ;
drawer . testEvents . addHandler ( 'create-data' , checkNotCalled ) ;
await viewer . world . requestInvalidate ( false ) ;
testDrawingRoutine ( 45 ) ;
for ( let tile of tileCache . _tilesLoaded ) {
const origCache = tile . getCache ( tile . originalCacheKey ) ;
test . equal ( origCache . type , T _A , "Original cache data was not affected, the drawer uses main cache even after refresh." ) ;
test . equal ( origCache . data , 0 , "Original cache data was not affected, the drawer uses main cache even after refresh." ) ;
}
test . ok ( testTileCalled , "Drawer tested at least one tile." ) ;
viewer . destroy ( ) ;
done ( ) ;
} ) ;
viewer . open ( [
{ isTestSource : true } ,
{ isTestSource : true } ,
] ) ;
} ) ;
2023-11-26 21:32:26 +01:00
//Tile API and cache interaction
QUnit . test ( 'Tile API Cache Interaction' , function ( test ) {
const done = test . async ( ) ;
2024-03-05 10:48:07 +01:00
const fakeViewer = MockSeadragon . getViewer (
MockSeadragon . getDrawer ( {
2024-02-05 09:42:26 +01:00
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats ( ) {
return [ T _A , T _B , T _C , T _D , T _E ] ;
}
2024-03-05 10:48:07 +01:00
} )
) ;
const tileCache = fakeViewer . tileCache ;
const fakeTiledImage0 = MockSeadragon . getTiledImage ( fakeViewer ) ;
const fakeTiledImage1 = MockSeadragon . getTiledImage ( fakeViewer ) ;
2023-11-26 21:32:26 +01:00
//load data
2024-03-05 10:48:07 +01:00
const tile00 = MockSeadragon . getTile ( 'foo.jpg' , fakeTiledImage0 ) ;
2024-03-03 14:50:01 +01:00
tile00 . addCache ( tile00 . cacheKey , 0 , T _A , false , false ) ;
2024-03-05 10:48:07 +01:00
const tile01 = MockSeadragon . getTile ( 'foo2.jpg' , fakeTiledImage0 ) ;
2024-03-03 14:50:01 +01:00
tile01 . addCache ( tile01 . cacheKey , 0 , T _B , false , false ) ;
2024-03-05 10:48:07 +01:00
const tile10 = MockSeadragon . getTile ( 'foo3.jpg' , fakeTiledImage1 ) ;
2024-03-03 14:50:01 +01:00
tile10 . addCache ( tile10 . cacheKey , 0 , T _C , false , false ) ;
2024-03-05 10:48:07 +01:00
const tile11 = MockSeadragon . getTile ( 'foo3.jpg' , fakeTiledImage1 ) ;
2024-03-03 14:50:01 +01:00
tile11 . addCache ( tile11 . cacheKey , 0 , T _C , false , false ) ;
2024-03-05 10:48:07 +01:00
const tile12 = MockSeadragon . getTile ( 'foo.jpg' , fakeTiledImage1 ) ;
2024-03-03 14:50:01 +01:00
tile12 . addCache ( tile12 . cacheKey , 0 , T _A , false , false ) ;
2023-11-26 21:32:26 +01:00
//test set/get data in async env
( async function ( ) {
2024-10-17 12:10:04 +02:00
test . equal ( tileCache . numTilesLoaded ( ) , 5 , "We loaded 5 tiles" ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 3 , "We loaded 3 cache objects - three different urls" ) ;
const c00 = tile00 . getCache ( tile00 . cacheKey ) ;
const c12 = tile12 . getCache ( tile12 . cacheKey ) ;
//now test multi-cache within tile
const theTileKey = tile00 . cacheKey ;
2024-11-21 15:35:27 +01:00
tile00 . addCache ( tile00 . buildDistinctMainCacheKey ( ) , 42 , T _E , true , false ) ;
2024-10-17 12:10:04 +02:00
test . notEqual ( tile00 . cacheKey , tile00 . originalCacheKey , "New cache rendered." ) ;
//now add artifically another record
tile00 . addCache ( "my_custom_cache" , 128 , T _C ) ;
test . equal ( tileCache . numTilesLoaded ( ) , 5 , "We still loaded only 5 tiles." ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 5 , "The cache has now 5 items (two new added already)." ) ;
test . equal ( c00 . getTileCount ( ) , 2 , "The cache still has only two tiles attached." ) ;
test . equal ( tile00 . getCacheSize ( ) , 3 , "The tile has three cache objects (original data, main cache & custom." ) ;
//related tile not affected
2024-11-21 15:35:27 +01:00
test . equal ( tile12 . cacheKey , tile12 . originalCacheKey , "Original cache change not reflected on shared caches." ) ;
2024-10-17 12:10:04 +02:00
test . equal ( tile12 . originalCacheKey , theTileKey , "Original cache key also preserved." ) ;
test . equal ( c12 . getTileCount ( ) , 2 , "The original data cache still has only two tiles attached." ) ;
2024-10-18 14:38:04 +02:00
//add and delete cache nothing changes (+1 destroy T_C)
tile00 . addCache ( "my_custom_cache2" , 128 , T _C ) ;
tile00 . removeCache ( "my_custom_cache2" ) ;
test . equal ( tileCache . numTilesLoaded ( ) , 5 , "We still loaded only 5 tiles." ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 5 , "The cache has now 5 items." ) ;
test . equal ( tile00 . getCacheSize ( ) , 3 , "The tile has three cache objects." ) ;
//delete cache as a zombie (+0 destroy)
tile00 . addCache ( "my_custom_cache2" , 17 , T _D ) ;
//direct access shoes correct value although we set key!
const myCustomCache2Data = tile00 . getCache ( "my_custom_cache2" ) . data ;
test . equal ( myCustomCache2Data , 17 , "Previously defined cache does not intervene." ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 6 , "The cache size is 6." ) ;
//keep zombie
tile00 . removeCache ( "my_custom_cache2" , false ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 6 , "The cache is 5 + 1 zombie, no change." ) ;
test . equal ( tile00 . getCacheSize ( ) , 3 , "The tile has three cache objects." ) ;
test . equal ( tileCache . _zombiesLoadedCount , 1 , "One zombie." ) ;
//revive zombie
tile01 . addCache ( "my_custom_cache2" , 18 , T _D ) ;
const myCustomCache2OtherData = tile01 . getCache ( "my_custom_cache2" ) . data ;
test . equal ( myCustomCache2OtherData , myCustomCache2Data , "Caches are equal because revived." ) ;
test . equal ( tileCache . _cachesLoadedCount , 6 , "Zombie revived, original state restored." ) ;
test . equal ( tileCache . _zombiesLoadedCount , 0 , "No zombies." ) ;
//again, keep zombie
tile01 . removeCache ( "my_custom_cache2" , false ) ;
//first create additional cache so zombie is not the youngest
tile01 . addCache ( "some weird cache" , 11 , T _A ) ;
test . ok ( tile01 . cacheKey === tile01 . originalCacheKey , "Custom cache does not touch tile cache keys." ) ;
//insertion aadditional cache clears the zombie first although it is not the youngest one
test . equal ( tileCache . numCachesLoaded ( ) , 7 , "The cache has now 7 items." ) ;
test . equal ( tileCache . _cachesLoadedCount , 6 , "New cache created -> 5+1." ) ;
test . equal ( tileCache . _zombiesLoadedCount , 1 , "One zombie remains." ) ;
//Test CAP
tileCache . _maxCacheItemCount = 7 ;
// Zombie destroyed before other caches (+1 destroy T_D)
2024-11-21 15:35:27 +01:00
tile12 . addCache ( "someKey" , 43 , T _B ) ;
2024-10-18 14:38:04 +02:00
test . equal ( tileCache . numCachesLoaded ( ) , 7 , "The cache has now 7 items." ) ;
test . equal ( tileCache . _zombiesLoadedCount , 0 , "One zombie sacrificed, preferred over living cache." ) ;
test . notOk ( [ tile00 , tile01 , tile10 , tile11 , tile12 ] . find ( x => ! x . loaded ) , "All tiles sill loaded since zombie was sacrificed." ) ;
// test destructors called as expected
test . equal ( destroyA , 0 , "No destructors for A called." ) ;
test . equal ( destroyB , 0 , "No destructors for B called." ) ;
test . equal ( destroyC , 1 , "One destruction for C called." ) ;
test . equal ( destroyD , 1 , "One destruction for D called." ) ;
test . equal ( destroyE , 0 , "No destructors for E called." ) ;
//try to revive zombie will fail: the zombie was deleted, we will find new vaue there
tile01 . addCache ( "my_custom_cache2" , - 849613 , T _C ) ;
const myCustomCache2RecreatedData = tile01 . getCache ( "my_custom_cache2" ) . data ;
test . notEqual ( myCustomCache2RecreatedData , myCustomCache2Data , "Caches are not equal because zombie was killed." ) ;
test . equal ( myCustomCache2RecreatedData , - 849613 , "Cache data is actually as set to 18." ) ;
test . equal ( tileCache . numCachesLoaded ( ) , 7 , "The cache has still 7 items." ) ;
// some tile has been selected as a sacrifice since we triggered cap control
test . ok ( [ tile00 , tile01 , tile10 , tile11 , tile12 ] . find ( x => ! x . loaded ) , "One tile has been sacrificed." ) ;
2024-10-17 12:10:04 +02:00
done ( ) ;
2023-11-26 21:32:26 +01:00
} ) ( ) ;
} ) ;
2023-11-18 20:16:35 +01:00
QUnit . test ( 'Zombie Cache' , function ( test ) {
const done = test . async ( ) ;
2025-01-10 22:05:16 +01:00
viewer = OpenSeadragon ( {
id : 'example' ,
prefixUrl : '/build/openseadragon/images/' ,
maxImageCacheCount : 200 , //should be enough to fit test inside the cache
springStiffness : 100 , // Faster animation = faster tests
drawer : 'test-cache-drawer-sync' ,
} ) ;
2024-10-17 12:10:04 +02:00
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
let jobCounter = 0 , coverage = undefined ;
OpenSeadragon . ImageLoader . prototype . addJob = function ( options ) {
jobCounter ++ ;
if ( coverage ) {
//old coverage of previous tiled image: if loaded, fail --> should be in cache
const coverageItem = coverage [ options . tile . level ] [ options . tile . x ] [ options . tile . y ] ;
test . ok ( ! coverageItem , "Attempt to add job for tile that should be already in memory." ) ;
}
return originalJob . call ( this , options ) ;
} ;
let tilesFinished = 0 ;
const tileCounter = function ( event ) { tilesFinished ++ ; }
const openHandler = function ( event ) {
event . item . allowZombieCache ( true ) ;
viewer . world . removeHandler ( 'add-item' , openHandler ) ;
test . ok ( jobCounter === 0 , 'Initial state, no images loaded' ) ;
waitFor ( ( ) => {
if ( tilesFinished === jobCounter && event . item . _fullyLoaded ) {
coverage = $ . extend ( true , { } , event . item . coverage ) ;
viewer . world . removeAll ( ) ;
return true ;
}
return false ;
} ) ;
} ;
let jobsAfterRemoval = 0 ;
const removalHandler = function ( event ) {
viewer . world . removeHandler ( 'remove-item' , removalHandler ) ;
test . ok ( jobCounter > 0 , 'Tiled image removed after 100 ms, should load some images.' ) ;
jobsAfterRemoval = jobCounter ;
viewer . world . addHandler ( 'add-item' , reopenHandler ) ;
viewer . addTiledImage ( {
tileSource : '/test/data/testpattern.dzi'
} ) ;
}
const reopenHandler = function ( event ) {
event . item . allowZombieCache ( true ) ;
viewer . removeHandler ( 'add-item' , reopenHandler ) ;
test . equal ( jobCounter , jobsAfterRemoval , 'Reopening image does not fetch any tiles imemdiatelly.' ) ;
waitFor ( ( ) => {
if ( event . item . _fullyLoaded ) {
viewer . removeHandler ( 'tile-unloaded' , unloadTileHandler ) ;
viewer . removeHandler ( 'tile-loaded' , tileCounter ) ;
coverage = undefined ;
//console test needs here explicit removal to finish correctly
OpenSeadragon . ImageLoader . prototype . addJob = originalJob ;
done ( ) ;
return true ;
}
return false ;
} ) ;
} ;
const unloadTileHandler = function ( event ) {
test . equal ( event . destroyed , false , "Tile unload event should not delete with zombies!" ) ;
}
viewer . world . addHandler ( 'add-item' , openHandler ) ;
viewer . world . addHandler ( 'remove-item' , removalHandler ) ;
viewer . addHandler ( 'tile-unloaded' , unloadTileHandler ) ;
viewer . addHandler ( 'tile-loaded' , tileCounter ) ;
viewer . open ( '/test/data/testpattern.dzi' ) ;
2023-11-18 20:16:35 +01:00
} ) ;
QUnit . test ( 'Zombie Cache Replace Item' , function ( test ) {
const done = test . async ( ) ;
2025-01-10 22:05:16 +01:00
viewer = OpenSeadragon ( {
id : 'example' ,
prefixUrl : '/build/openseadragon/images/' ,
maxImageCacheCount : 200 , //should be enough to fit test inside the cache
springStiffness : 100 , // Faster animation = faster tests
drawer : 'test-cache-drawer-sync' ,
} ) ;
2024-10-17 12:10:04 +02:00
let jobCounter = 0 , coverage = undefined ;
OpenSeadragon . ImageLoader . prototype . addJob = function ( options ) {
jobCounter ++ ;
if ( coverage ) {
//old coverage of previous tiled image: if loaded, fail --> should be in cache
const coverageItem = coverage [ options . tile . level ] [ options . tile . x ] [ options . tile . y ] ;
if ( ! coverageItem ) {
console . warn ( coverage , coverage [ options . tile . level ] [ options . tile . x ] , options . tile ) ;
}
test . ok ( ! coverageItem , "Attempt to add job for tile data that was previously loaded." ) ;
}
return originalJob . call ( this , options ) ;
} ;
let tilesFinished = 0 ;
const tileCounter = function ( event ) { tilesFinished ++ ; }
const openHandler = function ( event ) {
event . item . allowZombieCache ( true ) ;
viewer . world . removeHandler ( 'add-item' , openHandler ) ;
viewer . world . addHandler ( 'add-item' , reopenHandler ) ;
waitFor ( ( ) => {
if ( tilesFinished === jobCounter && event . item . _fullyLoaded ) {
coverage = $ . extend ( true , { } , event . item . coverage ) ;
viewer . addTiledImage ( {
tileSource : '/test/data/testpattern.dzi' ,
index : 0 ,
replace : true
} ) ;
return true ;
}
return false ;
} ) ;
} ;
const reopenHandler = function ( event ) {
event . item . allowZombieCache ( true ) ;
viewer . removeHandler ( 'add-item' , reopenHandler ) ;
waitFor ( ( ) => {
if ( event . item . _fullyLoaded ) {
viewer . removeHandler ( 'tile-unloaded' , unloadTileHandler ) ;
viewer . removeHandler ( 'tile-loaded' , tileCounter ) ;
//console test needs here explicit removal to finish correctly
OpenSeadragon . ImageLoader . prototype . addJob = originalJob ;
done ( ) ;
return true ;
}
return false ;
} ) ;
} ;
const unloadTileHandler = function ( event ) {
test . equal ( event . destroyed , false , "Tile unload event should not delete with zombies!" ) ;
}
viewer . world . addHandler ( 'add-item' , openHandler ) ;
viewer . addHandler ( 'tile-unloaded' , unloadTileHandler ) ;
viewer . addHandler ( 'tile-loaded' , tileCounter ) ;
viewer . open ( '/test/data/testpattern.dzi' ) ;
2023-11-18 20:16:35 +01:00
} ) ;
2014-11-20 11:51:24 -08:00
} ) ( ) ;