generateFunctionMocks = (proto, itemMainProp, dataArray) => ({ item: utils.createProxy(proto.item, { apply(target, ctx, args) { if (!args.length) { throw new TypeError( `Failed to execute 'item' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.` ); } // Special behavior alert: // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup // - If anything else than an integer (including as string) is provided it will return the first entry const isInteger = args[0] && Number.isInteger(Number(args[0])); // Cast potential string to number first, then check for integer // Note: Vanilla never returns `undefined` return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null; } }), /** Returns the MimeType object with the specified name. */ namedItem: utils.createProxy(proto.namedItem, { apply(target, ctx, args) { if (!args.length) { throw new TypeError( `Failed to execute 'namedItem' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.` ); } return dataArray.find((mt) => mt[itemMainProp] === args[0]) || null; // Not `undefined`! } }), /** Does nothing and shall return nothing */ refresh: proto.refresh ? utils.createProxy(proto.refresh, { apply(target, ctx, args) { return undefined; } }) : undefined }); function generateMagicArray( dataArray = [], proto = MimeTypeArray.prototype, itemProto = MimeType.prototype, itemMainProp = "type" ) { // Quick helper to set props with the same descriptors vanilla is using const defineProp = (obj, prop, value) => Object.defineProperty(obj, prop, { value, writable: false, enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)` configurable: false }); // Loop over our fake data and construct items const makeItem = (data) => { const item = {}; for (const prop of Object.keys(data)) { if (prop.startsWith("__")) { continue; } defineProp(item, prop, data[prop]); } // navigator.plugins[i].length should always be 1 if (itemProto === Plugin.prototype) { defineProp(item, "length", 1); } // We need to spoof a specific `MimeType` or `Plugin` object return Object.create(itemProto, Object.getOwnPropertyDescriptors(item)); }; const magicArray = []; // Loop through our fake data and use that to create convincing entities dataArray.forEach((data) => { magicArray.push(makeItem(data)); }); // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards magicArray.forEach((entry) => { defineProp(magicArray, entry[itemMainProp], entry); }); // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)` const magicArrayObj = Object.create(proto, { ...Object.getOwnPropertyDescriptors(magicArray), // There's one ugly quirk we unfortunately need to take care of: // The `MimeTypeArray` prototype has an enumerable `length` property, // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`. // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap. length: { value: magicArray.length, writable: false, enumerable: false, configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length` } }); // Generate our functional function mocks :-) const functionMocks = generateFunctionMocks(proto, itemMainProp, magicArray); // Override custom object with proxy return new Proxy(magicArrayObj, { get(target, key = "") { // Redirect function calls to our custom proxied versions mocking the vanilla behavior if (key === "item") { return functionMocks.item; } if (key === "namedItem") { return functionMocks.namedItem; } if (proto === PluginArray.prototype && key === "refresh") { return functionMocks.refresh; } // Everything else can pass through as normal return utils.cache.Reflect.get(...arguments); }, ownKeys(target) { // There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length` // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing const keys = []; const typeProps = magicArray.map((mt) => mt[itemMainProp]); typeProps.forEach((_, i) => keys.push(`${i}`)); typeProps.forEach((propName) => keys.push(propName)); return keys; } }); }