From d69e24420d9ef4e69315d5dc590ee7e047706aa2 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Thu, 24 Oct 2019 16:23:47 -0700 Subject: [PATCH] Add `deferred` option `setRTLTextPlugin` to load the plugin only when the first time RTL text is encountered. (#8865) --- debug/rtl.html | 37 +++++- src/data/bucket/symbol_bucket.js | 18 ++- src/index.js | 2 + src/source/rtl_text_plugin.js | 132 ++++++++++++++----- src/source/tile.js | 15 +++ src/source/vector_tile_source.js | 10 ++ src/source/worker.js | 22 +++- src/style-spec/expression/types/formatted.js | 18 +++ src/style/style.js | 34 +++-- src/types/window.js | 1 - src/util/ajax.js | 14 +- src/util/script_detection.js | 22 +++- src/util/util.js | 12 ++ src/util/window.js | 9 ++ test/unit/style/style.test.js | 27 ++-- 15 files changed, 288 insertions(+), 85 deletions(-) diff --git a/debug/rtl.html b/debug/rtl.html index 6257d484629..162eeb500f0 100644 --- a/debug/rtl.html +++ b/debug/rtl.html @@ -8,16 +8,29 @@
+
+
+
+ Plugin Status:
+ +
diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index c34f8d880b4..7f7e48f09f1 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -37,6 +37,7 @@ import {register} from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; import Formatted from '../../style-spec/expression/types/formatted'; import ResolvedImage from '../../style-spec/expression/types/resolved_image'; +import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin'; import type { Bucket, @@ -305,6 +306,7 @@ class SymbolBucket implements Bucket { writingModes: Array; allowVerticalPlacement: boolean; hasPaintOverrides: boolean; + hasRTLText: boolean; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -317,6 +319,7 @@ class SymbolBucket implements Bucket { this.sourceLayerIndex = options.sourceLayerIndex; this.hasPattern = false; this.hasPaintOverrides = false; + this.hasRTLText = false; const layer = this.layers[0]; const unevaluatedLayoutValues = layer._unevaluatedLayout._values; @@ -409,10 +412,17 @@ class SymbolBucket implements Bucket { // but plain string token evaluation skips that pathway so do the // conversion here. const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature, availableImages); - text = transformText(resolvedTokens instanceof Formatted ? - resolvedTokens : - Formatted.fromString(resolvedTokens), - layer, feature); + const formattedText = Formatted.factory(resolvedTokens); + if (formattedText.containsRTLText()) { + this.hasRTLText = true; + } + if ( + !this.hasRTLText || // non-rtl text so can proceed safely + getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping + this.hasRTLText && globalRTLTextPlugin.isParsed() // Use the rtlText plugin to shape text + ) { + text = transformText(formattedText, layer, feature); + } } let icon: ResolvedImage | void; diff --git a/src/index.js b/src/index.js index bc9fe4387ab..ccd3ca97b01 100644 --- a/src/index.js +++ b/src/index.js @@ -158,6 +158,8 @@ const exported = { * @function setRTLTextPlugin * @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source. * @param {Function} callback Called with an error argument if there is an error. + * @param {boolean} lazy If set to `true`, mapboxgl will defer loading the plugin until rtl text is encountered, + * rtl text will then be rendered only after the plugin finishes loading. * @example * mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js'); * @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/) diff --git a/src/source/rtl_text_plugin.js b/src/source/rtl_text_plugin.js index 2ca83880d31..8681a98536f 100644 --- a/src/source/rtl_text_plugin.js +++ b/src/source/rtl_text_plugin.js @@ -1,77 +1,143 @@ // @flow import {Event, Evented} from '../util/evented'; +import {getArrayBuffer} from '../util/ajax'; import browser from '../util/browser'; +import window from '../util/window'; +import assert from 'assert'; +import {isWorker} from '../util/util'; const status = { - unavailable: 'unavailable', - loading: 'loading', + unavailable: 'unavailable', // Not loaded + deferred: 'deferred', // The plugin URL has been specified, but loading has been deferred + loading: 'loading', // request in-flight loaded: 'loaded', error: 'error' }; + +export type PluginState = { + pluginStatus: $Values; + pluginURL: ?string, + pluginBlobURL: ?string +}; + +type ErrorCallback = (error: ?Error) => void; +type PluginStateSyncCallback = (state: PluginState) => void; +let _completionCallback = null; + +//Variables defining the current state of the plugin let pluginStatus = status.unavailable; let pluginURL = null; +let pluginBlobURL = null; -export const evented = new Evented(); +export const triggerPluginCompletionEvent = function(error: ?Error) { + if (_completionCallback) { + _completionCallback(error); + } +}; -type CompletionCallback = (error?: Error) => void; -type ErrorCallback = (error: Error) => void; +function sendPluginStateToWorker() { + evented.fire(new Event('pluginStateChange', {pluginStatus, pluginURL, pluginBlobURL})); +} -let _completionCallback; +export const evented = new Evented(); export const getRTLTextPluginStatus = function () { return pluginStatus; }; -export const registerForPluginAvailability = function( - callback: (args: {pluginURL: string, completionCallback: CompletionCallback}) => void -) { - if (pluginURL) { - callback({pluginURL, completionCallback: _completionCallback}); - } else { - evented.once('pluginAvailable', callback); - } +export const registerForPluginStateChange = function(callback: PluginStateSyncCallback) { + // Do an initial sync of the state + callback({pluginStatus, pluginURL, pluginBlobURL}); + // Listen for all future state changes + evented.on('pluginStateChange', callback); return callback; }; export const clearRTLTextPlugin = function() { pluginStatus = status.unavailable; pluginURL = null; + if (pluginBlobURL) { + window.URL.revokeObjectURL(pluginBlobURL); + } + pluginBlobURL = null; }; -export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) { - if (pluginStatus === status.loading || pluginStatus === status.loaded) { +export const setRTLTextPlugin = function(url: string, callback: ?ErrorCallback, deferred: boolean = false) { + if (pluginStatus === status.deferred || pluginStatus === status.loading || pluginStatus === status.loaded) { throw new Error('setRTLTextPlugin cannot be called multiple times.'); } - pluginStatus = status.loading; pluginURL = browser.resolveURL(url); - _completionCallback = (error?: Error) => { - if (error) { - // Clear loaded state to allow retries - clearRTLTextPlugin(); - pluginStatus = status.error; - if (callback) { - callback(error); + pluginStatus = status.deferred; + _completionCallback = callback; + sendPluginStateToWorker(); + + //Start downloading the plugin immediately if not intending to lazy-load + if (!deferred) { + downloadRTLTextPlugin(); + } +}; + +export const downloadRTLTextPlugin = function() { + if (pluginStatus !== status.deferred || !pluginURL) { + throw new Error('rtl-text-plugin cannot be downloaded unless a pluginURL is specified'); + } + pluginStatus = status.loading; + sendPluginStateToWorker(); + if (pluginURL) { + getArrayBuffer({url: pluginURL}, (error, data) => { + if (error) { + triggerPluginCompletionEvent(error); + } else { + const rtlBlob = new window.Blob([data], {type: 'application/javascript'}); + pluginBlobURL = window.URL.createObjectURL(rtlBlob); + pluginStatus = status.loaded; + sendPluginStateToWorker(); } - } else { - // Called once for each worker - pluginStatus = status.loaded; - } - }; - evented.fire(new Event('pluginAvailable', {pluginURL, completionCallback: _completionCallback})); + }); + } }; export const plugin: { applyArabicShaping: ?Function, processBidirectionalText: ?(string, Array) => Array, processStyledBidirectionalText: ?(string, Array, Array) => Array<[string, Array]>, - isLoaded: () => boolean + isLoaded: () => boolean, + isLoading: () => boolean, + setState: (state: PluginState) => void, + isParsed: () => boolean, + getURLs: () => { blob: ?string, host: ?string } } = { applyArabicShaping: null, processBidirectionalText: null, processStyledBidirectionalText: null, isLoaded() { - return pluginStatus === status.loaded || // Foreground: loaded if the completion callback returned successfully - plugin.applyArabicShaping != null; // Background: loaded if the plugin functions have been compiled + return pluginStatus === status.loaded || // Main Thread: loaded if the completion callback returned successfully + plugin.applyArabicShaping != null; // Web-worker: loaded if the plugin functions have been compiled + }, + isLoading() { // Main Thread Only: query the loading status, this function does not return the correct value in the worker context. + return pluginStatus === status.loading; + }, + setState(state: PluginState) { // Worker thread only: this tells the worker threads that the plugin is available on the Main thread + assert(isWorker(), 'Cannot set the state of the rtl-text-plugin when not in the web-worker context'); + + pluginStatus = state.pluginStatus; + pluginURL = state.pluginURL; + pluginBlobURL = state.pluginBlobURL; + }, + isParsed(): boolean { + assert(isWorker(), 'rtl-text-plugin is only parsed on the worker-threads'); + + return plugin.applyArabicShaping != null && + plugin.processBidirectionalText != null && + plugin.processStyledBidirectionalText != null; + }, + getURLs(): { blob: ?string, host: ?string } { + assert(isWorker(), 'rtl-text-plugin urls can only be queried from the worker threads'); + + return { + blob: pluginBlobURL, + host: pluginURL, + }; } }; diff --git a/src/source/tile.js b/src/source/tile.js index 9c394a5fd45..e714007e2e1 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -96,6 +96,7 @@ class Tile { symbolFadeHoldUntil: ?number; hasSymbolBuckets: boolean; + hasRTLText: boolean; /** * @param {OverscaledTileID} tileID @@ -110,6 +111,7 @@ class Tile { this.expirationTime = null; this.queryPadding = 0; this.hasSymbolBuckets = false; + this.hasRTLText = false; // Counts the number of times a response was already expired when // received. We're using this to add a delay when making a new request @@ -184,6 +186,19 @@ class Tile { } } + this.hasRTLText = false; + if (this.hasSymbolBuckets) { + for (const id in this.buckets) { + const bucket = this.buckets[id]; + if (bucket instanceof SymbolBucket) { + if (bucket.hasRTLText) { + this.hasRTLText = true; + break; + } + } + } + } + this.queryPadding = 0; for (const id in this.buckets) { const bucket = this.buckets[id]; diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 5c8d435db64..957c2ef6b72 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -9,6 +9,7 @@ import TileBounds from './tile_bounds'; import {ResourceType} from '../util/ajax'; import browser from '../util/browser'; import {cacheEntryPossiblyAdded} from '../util/tile_request_cache'; +import {plugin as rtlTextPlugin, getRTLTextPluginStatus, downloadRTLTextPlugin} from './rtl_text_plugin'; import type {Source} from './source'; import type {OverscaledTileID} from './tile_id'; @@ -153,6 +154,15 @@ class VectorTileSource extends Evented implements Source { if (this.map._refreshExpiredTiles && data) tile.setExpiryData(data); tile.loadVectorData(data, this.map.painter); + if (tile.hasRTLText) { + const plugin = rtlTextPlugin; + if (!plugin.isLoading() && + !plugin.isLoaded() && + getRTLTextPluginStatus() === 'deferred' + ) { + downloadRTLTextPlugin(); + } + } cacheEntryPossiblyAdded(this.dispatcher); diff --git a/src/source/worker.js b/src/source/worker.js index 7d6f494b58c..e62175c43e4 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -22,6 +22,7 @@ import type { import type {WorkerGlobalScopeInterface} from '../util/web_worker'; import type {Callback} from '../types/callback'; import type {LayerSpecification} from '../style-spec/types'; +import type {PluginState} from './rtl_text_plugin'; /** * @private @@ -59,8 +60,9 @@ export default class Worker { this.workerSourceTypes[name] = WorkerSource; }; + // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => { - if (globalRTLTextPlugin.isLoaded()) { + if (globalRTLTextPlugin.isParsed()) { throw new Error('RTL text plugin already registered.'); } globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; @@ -151,13 +153,19 @@ export default class Worker { } } - loadRTLTextPlugin(map: string, pluginURL: string, callback: Callback) { + syncRTLPluginState(map: string, state: PluginState, callback: Callback) { try { - if (!globalRTLTextPlugin.isLoaded()) { - this.self.importScripts(pluginURL); - callback(globalRTLTextPlugin.isLoaded() ? - null : - new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`)); + globalRTLTextPlugin.setState(state); + const {blob, host} = globalRTLTextPlugin.getURLs(); + if ( + globalRTLTextPlugin.isLoaded() && + !globalRTLTextPlugin.isParsed() && + blob != null && host != null // Not possible when `isLoaded` is true, but keeps flow happy + ) { + this.self.importScripts(blob); + const complete = globalRTLTextPlugin.isParsed(); + const error = complete ? undefined : new Error(`RTL Text Plugin failed to import scripts from ${host}`); + callback(error, complete); } } catch (e) { callback(e.toString()); diff --git a/src/style-spec/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js index 0bc5000b4cc..9029a89527d 100644 --- a/src/style-spec/expression/types/formatted.js +++ b/src/style-spec/expression/types/formatted.js @@ -1,5 +1,6 @@ // @flow +import {stringContainsRTLText} from "../../../util/script_detection"; import type Color from '../../util/color'; export class FormattedSection { @@ -27,10 +28,27 @@ export default class Formatted { return new Formatted([new FormattedSection(unformatted, null, null, null)]); } + static factory(text: Formatted | string): Formatted { + if (text instanceof Formatted) { + return text; + } else { + return Formatted.fromString(text); + } + } + toString(): string { return this.sections.map(section => section.text).join(''); } + containsRTLText(): boolean { + for (const section of this.sections) { + if (stringContainsRTLText(section.text)) { + return true; + } + } + return false; + } + serialize(): Array { const serialized = ["format"]; for (const section of this.sections) { diff --git a/src/style/style.js b/src/style/style.js index 41d6a934c86..6ab17885fb9 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -29,8 +29,9 @@ import getWorkerPool from '../util/global_worker_pool'; import deref from '../style-spec/deref'; import diffStyles, {operations as diffOperations} from '../style-spec/diff'; import { - registerForPluginAvailability, - evented as rtlTextPluginEvented + registerForPluginStateChange, + evented as rtlTextPluginEvented, + triggerPluginCompletionEvent } from '../source/rtl_text_plugin'; import PauseablePlacement from './pauseable_placement'; import ZoomHistory from './zoom_history'; @@ -129,7 +130,7 @@ class Style extends Evented { // exposed to allow stubbing by unit tests static getSourceType: typeof getSourceType; static setSourceType: typeof setSourceType; - static registerForPluginAvailability: typeof registerForPluginAvailability; + static registerForPluginStateChange: typeof registerForPluginStateChange; constructor(map: Map, options: StyleOptions = {}) { super(); @@ -153,11 +154,24 @@ class Style extends Evented { this.dispatcher.broadcast('setReferrer', getReferrer()); const self = this; - this._rtlTextPluginCallback = Style.registerForPluginAvailability((args) => { - self.dispatcher.broadcast('loadRTLTextPlugin', args.pluginURL, args.completionCallback); - for (const id in self.sourceCaches) { - self.sourceCaches[id].reload(); // Should be a no-op if the plugin loads before any tiles load - } + this._rtlTextPluginCallback = Style.registerForPluginStateChange((event) => { + const state = { + pluginStatus: event.pluginStatus, + pluginURL: event.pluginURL, + pluginBlobURL: event.pluginBlobURL + }; + self.dispatcher.broadcast('syncRTLPluginState', state, (err, results) => { + triggerPluginCompletionEvent(err); + if (results) { + const allComplete = results.every((elem) => elem); + if (allComplete) { + for (const id in self.sourceCaches) { + self.sourceCaches[id].reload(); // Should be a no-op if the plugin loads before any tiles load + } + } + } + + }); }); this.on('data', (event) => { @@ -1144,7 +1158,7 @@ class Style extends Evented { this._spriteRequest.cancel(); this._spriteRequest = null; } - rtlTextPluginEvented.off('pluginAvailable', this._rtlTextPluginCallback); + rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); for (const layerId in this._layers) { const layer: StyleLayer = this._layers[layerId]; layer.setEventedParent(null); @@ -1272,6 +1286,6 @@ class Style extends Evented { Style.getSourceType = getSourceType; Style.setSourceType = setSourceType; -Style.registerForPluginAvailability = registerForPluginAvailability; +Style.registerForPluginStateChange = registerForPluginStateChange; export default Style; diff --git a/src/types/window.js b/src/types/window.js index 27ed038086c..8957156fcc7 100644 --- a/src/types/window.js +++ b/src/types/window.js @@ -128,7 +128,6 @@ export interface Window extends EventTarget, IDBEnvironment { URL: typeof URL; URLSearchParams: typeof URLSearchParams; WebGLFramebuffer: typeof WebGLFramebuffer; - webkitURL: typeof URL; WheelEvent: typeof WheelEvent; Worker: typeof Worker; XMLHttpRequest: typeof XMLHttpRequest; diff --git a/src/util/ajax.js b/src/util/ajax.js index f581cfc0c1e..8c54ce69044 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -1,7 +1,7 @@ // @flow import window from './window'; -import {extend, warnOnce} from './util'; +import {extend, warnOnce, isWorker} from './util'; import {isMapboxHTTPURL, hasCacheDefeatingSku} from './mapbox'; import config from './config'; import assert from 'assert'; @@ -73,16 +73,11 @@ class AJAXError extends Error { } } -function isWorker() { - return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && - self instanceof WorkerGlobalScope; -} - // Ensure that we're sending the correct referrer from blob URL worker bundles. // For files loaded from the local file system, `location.origin` will be set // to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE), // and we will set an empty referrer. Otherwise, we're using the document's URL. -/* global self, WorkerGlobalScope */ +/* global self */ export const getReferrer = isWorker() ? () => self.worker && self.worker.referrer : () => (window.location.protocol === 'blob:' ? window.parent : window).location.href; @@ -312,16 +307,15 @@ export const getImage = function(requestParameters: RequestParameters, callback: callback(err); } else if (data) { const img: HTMLImageElement = new window.Image(); - const URL = window.URL || window.webkitURL; img.onload = () => { callback(null, img); - URL.revokeObjectURL(img.src); + window.URL.revokeObjectURL(img.src); }; img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); (img: any).cacheControl = cacheControl; (img: any).expires = expires; - img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; + img.src = data.byteLength ? window.URL.createObjectURL(blob) : transparentPngUrl; } }); diff --git a/src/util/script_detection.js b/src/util/script_detection.js index 5ae5b933ef5..985ae483001 100644 --- a/src/util/script_detection.js +++ b/src/util/script_detection.js @@ -277,6 +277,13 @@ export function charInComplexShapingScript(char: number) { isChar['Arabic Presentation Forms-B'](char); } +export function charInRTLScript(char: number) { + // Main blocks for Hebrew, Arabic, Thaana and other RTL scripts + return (char >= 0x0590 && char <= 0x08FF) || + isChar['Arabic Presentation Forms-A'](char) || + isChar['Arabic Presentation Forms-B'](char); +} + export function charInSupportedScript(char: number, canRenderRTL: boolean) { // This is a rough heuristic: whether we "can render" a script // actually depends on the properties of the font being used @@ -285,11 +292,7 @@ export function charInSupportedScript(char: number, canRenderRTL: boolean) { // Even in Latin script, we "can't render" combinations such as the fi // ligature, but we don't consider that semantically significant. - if (!canRenderRTL && - ((char >= 0x0590 && char <= 0x08FF) || - isChar['Arabic Presentation Forms-A'](char) || - isChar['Arabic Presentation Forms-B'](char))) { - // Main blocks for Hebrew, Arabic, Thaana and other RTL scripts + if (!canRenderRTL && charInRTLScript(char)) { return false; } if ((char >= 0x0900 && char <= 0x0DFF) || @@ -306,6 +309,15 @@ export function charInSupportedScript(char: number, canRenderRTL: boolean) { return true; } +export function stringContainsRTLText(chars: string): boolean { + for (const char of chars) { + if (charInRTLScript(char.charCodeAt(0))) { + return true; + } + } + return false; +} + export function isStringInSupportedScript(chars: string, canRenderRTL: boolean) { for (const char of chars) { if (!charInSupportedScript(char.charCodeAt(0), canRenderRTL)) { diff --git a/src/util/util.js b/src/util/util.js index 81a3c3706e7..9471aabb85d 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -409,6 +409,18 @@ export function sphericalToCartesian([r, azimuthal, polar]: [number, number, num }; } +/* global self, WorkerGlobalScope */ +/** + * Retuns true if the when run in the web-worker context. + * + * @private + * @returns {boolean} + */ +export function isWorker(): boolean { + return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && + self instanceof WorkerGlobalScope; +} + /** * Parses data from 'Cache-Control' headers. * diff --git a/src/util/window.js b/src/util/window.js index 618f2919f49..684a170f7cd 100644 --- a/src/util/window.js +++ b/src/util/window.js @@ -76,6 +76,15 @@ function restore(): Window { window.URL.revokeObjectURL = function () {}; + window.fakeWorkerPresence = function() { + global.WorkerGlobalScope = function() {}; + global.self = new global.WorkerGlobalScope(); + }; + window.clearFakeWorkerPresence = function() { + global.WorkerGlobalScope = undefined; + global.self = undefined; + }; + window.restore = restore; window.ImageData = window.ImageData || function() { return false; }; diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index 18f4d6e57fb..9da9e12fa89 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -62,23 +62,31 @@ test('Style', (t) => { callback(); }); - t.test('registers plugin listener', (t) => { + t.test('registers plugin state change listener', (t) => { clearRTLTextPlugin(); - - t.spy(Style, 'registerForPluginAvailability'); - + window.useFakeXMLHttpRequest(); + window.fakeWorkerPresence(); + t.spy(Style, 'registerForPluginStateChange'); const style = new Style(new StubMap()); t.spy(style.dispatcher, 'broadcast'); - t.ok(Style.registerForPluginAvailability.calledOnce); + t.ok(Style.registerForPluginStateChange.calledOnce); - setRTLTextPlugin("some bogus url"); - t.ok(style.dispatcher.broadcast.calledWith('loadRTLTextPlugin', "some bogus url")); + setRTLTextPlugin("/plugin.js",); + t.ok(style.dispatcher.broadcast.calledWith('syncRTLPluginState', { + pluginStatus: 'deferred', + pluginURL: "/plugin.js", + pluginBlobURL: null + })); + window.clearFakeWorkerPresence(); t.end(); }); t.test('loads plugin immediately if already registered', (t) => { clearRTLTextPlugin(); window.useFakeXMLHttpRequest(); + window.fakeWorkerPresence(); + window.URL.createObjectURL = () => 'blob:'; + t.tearDown(() => delete window.URL.createObjectURL); window.server.respondWith('/plugin.js', "doesn't matter"); let firstError = true; setRTLTextPlugin("/plugin.js", (error) => { @@ -87,6 +95,7 @@ test('Style', (t) => { if (firstError) { t.equals(error.message, 'RTL Text Plugin failed to import scripts from /plugin.js'); t.end(); + window.clearFakeWorkerPresence(); firstError = false; } }); @@ -421,8 +430,8 @@ test('Style#_remove', (t) => { style.on('style.load', () => { style._remove(); - rtlTextPluginEvented.fire(new Event('pluginAvailable')); - t.notOk(style.dispatcher.broadcast.calledWith('loadRTLTextPlugin')); + rtlTextPluginEvented.fire(new Event('pluginStateChange')); + t.notOk(style.dispatcher.broadcast.calledWith('syncRTLPluginState')); t.end(); }); });