UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

296 lines 13.6 kB
/* @license * Copyright 2019 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { IS_IE11 } from '../constants.js'; import ModelViewerElementBase, { $canvas, $scene } from '../model-viewer-base.js'; import { Renderer } from '../three-components/Renderer.js'; import { resolveDpr } from '../utilities.js'; import { assetPath, spy, timePasses, until, waitForEvent } from './helpers.js'; import { BasicSpecTemplate } from './templates.js'; const expect = chai.expect; const expectBlobDimensions = async (blob, width, height) => { const img = await new Promise((resolve) => { const url = URL.createObjectURL(blob); const img = new Image(); img.onload = () => { resolve(img); }; img.src = url; }); const dpr = resolveDpr(); expect(img.width).to.be.equal(width * dpr); expect(img.height).to.be.equal(height * dpr); }; suite('ModelViewerElementBase', () => { test('is not registered as a custom element by default', () => { expect(customElements.get('model-viewer-base')).to.be.equal(undefined); }); suite('when registered', () => { let nextId = 0; let tagName; let ModelViewerElement; setup(() => { tagName = `model-viewer-${nextId++}`; ModelViewerElement = class extends ModelViewerElementBase { static get is() { return tagName; } }; customElements.define(tagName, ModelViewerElement); }); BasicSpecTemplate(() => ModelViewerElement, () => tagName); suite('with alt text', () => { let element; let canvas; setup(() => { element = new ModelViewerElement(); canvas = element[$canvas]; document.body.appendChild(element); }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); test('gives the canvas a related aria-label', async () => { const altText = 'foo'; const canvas = element[$canvas]; element.alt = altText; await timePasses(); expect(canvas.getAttribute('aria-label')).to.be.equal(altText); }); suite('that is removed', () => { test('reverts canvas to default aria-label', async () => { const defaultAriaLabel = canvas.getAttribute('aria-label'); const altText = 'foo'; element.alt = altText; await timePasses(); element.alt = null; await timePasses(); expect(canvas.getAttribute('aria-label')) .to.be.equal(defaultAriaLabel); }); }); }); suite('with a valid src', () => { let element; setup(() => { element = new ModelViewerElement(); document.body.appendChild(element); }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); test('eventually dispatches a load event', async () => { const sourceLoads = waitForEvent(element, 'load'); element.src = assetPath('Astronaut.glb'); await sourceLoads; }); suite('that changes before the model loads', () => { test('it loads the second value on microtask timing', async () => { element.src = assetPath('Astronaut.glb'); await timePasses(); element.src = assetPath('Horse.glb'); await waitForEvent(element, 'load'); expect(element[$scene].model.userData.url) .to.be.equal(assetPath('Horse.glb')); }); test('it loads the second value on task timing', async () => { element.src = assetPath('Astronaut.glb'); await timePasses(1); element.src = assetPath('Horse.glb'); await waitForEvent(element, 'load'); expect(element[$scene].model.userData.url) .to.be.equal(assetPath('Horse.glb')); }); }); }); suite('with an invalid src', () => { let element; setup(() => { element = new ModelViewerElement(); document.body.appendChild(element); }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); test('eventually dispatches an error event', async () => { const sourceErrors = waitForEvent(element, 'error'); element.src = './does-not-exist.glb'; await sourceErrors; }); }); suite('when losing the GL context', () => { let element; setup(() => { element = new ModelViewerElement(); document.body.appendChild(element); }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); test('dispatches a related error event', async () => { const { threeRenderer } = Renderer.singleton; const errorEventDispatches = waitForEvent(element, 'error'); // We make a best effor to simulate the real scenario here, but // for some cases like headless Chrome WebGL might be disabled, // so we simulate the scenario. // @see https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer.forceContextLoss if (threeRenderer.context != null && !IS_IE11) { threeRenderer.forceContextLoss(); } else { threeRenderer.domElement.dispatchEvent(new CustomEvent('webglcontextlost')); } const event = await errorEventDispatches; expect(event.detail.type).to.be.equal('webglcontextlost'); }); }); suite('capturing screenshots', () => { let element; setup(async () => { element = new ModelViewerElement(); // Avoid testing our memory ceiling in CI by limiting the size // of the screenshots we produce in these tests: element.style.width = '32px'; element.style.height = '64px'; document.body.appendChild(element); const modelLoads = waitForEvent(element, 'load'); element.src = assetPath('cube.gltf'); await modelLoads; }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); suite('toDataURL', () => { test('produces a URL-compatible string', () => { const dataUrlMatcher = /^data\:image\//; expect(dataUrlMatcher.test(element.toDataURL())).to.be.true; }); }); suite('toBlob', () => { test('produces a blob', async () => { const blob = await element.toBlob(); expect(blob).to.not.be.null; }); test('can convert blob to object URL', async () => { const blob = await element.toBlob(); const objectUrl = URL.createObjectURL(blob); const objectUrlMatcher = /^blob\:/; expect(objectUrlMatcher.test(objectUrl)).to.be.true; }); test('has size', async () => { const blob = await element.toBlob(); expect(blob.size).to.be.greaterThan(0); }); test('uses fallbacks on unsupported browsers', async () => { // Emulate unsupported browser let restoreCanvasToBlob = () => { }; try { restoreCanvasToBlob = spy(HTMLCanvasElement.prototype, 'toBlob', { value: undefined }); } catch (error) { // Ignored... } const blob = await element.toBlob(); expect(blob).to.not.be.null; restoreCanvasToBlob(); }); test('blobs on supported and unsupported browsers are equivalent', async () => { // Skip test on IE11 since it doesn't have Response to fetch // arrayBuffer if (IS_IE11) { return; } let restoreCanvasToBlob = () => { }; try { restoreCanvasToBlob = spy(HTMLCanvasElement.prototype, 'toBlob', { value: undefined }); } catch (error) { // Ignored... } const unsupportedBrowserBlob = await element.toBlob(); restoreCanvasToBlob(); const supportedBrowserBlob = await element.toBlob(); // Blob.prototype.arrayBuffer is not available in Edge / Safari // Using Response to get arrayBuffer instead const supportedBrowserResponse = new Response(supportedBrowserBlob); const unsupportedBrowserResponse = new Response(unsupportedBrowserBlob); const supportedBrowserArrayBuffer = await supportedBrowserResponse.arrayBuffer(); const unsupportedBrowserArrayBuffer = await unsupportedBrowserResponse.arrayBuffer(); expect(unsupportedBrowserArrayBuffer) .to.eql(supportedBrowserArrayBuffer); }); test('idealAspect gives the proper blob dimensions', async () => { const basicBlob = await element.toBlob(); const idealBlob = await element.toBlob({ idealAspect: true }); const idealHeight = Math.round(32 / element[$scene].model.fieldOfViewAspect); await expectBlobDimensions(basicBlob, 32, 64); await expectBlobDimensions(idealBlob, 32, idealHeight); }); }); }); suite('orchestrates rendering', () => { let elements = []; setup(async () => { elements.push(new ModelViewerElement()); elements.push(new ModelViewerElement()); elements.push(new ModelViewerElement()); const loaded = elements.map(e => waitForEvent(e, 'load')); for (let element of elements) { element.style.position = 'relative'; element.style.marginBottom = '100vh'; element.src = assetPath('cube.gltf'); document.body.appendChild(element); } await Promise.all(loaded); }); teardown(() => { elements.forEach(element => element.parentNode != null && element.parentNode.removeChild(element)); }); test('sets a model within viewport to be visible', async () => { await until(() => { return elements[0][$scene].visible; }); expect(elements[0][$scene].visible).to.be.true; }); test.skip('only models visible in the viewport', async () => { // IntersectionObserver needs to set appropriate // visibility on the scene, lots of timing issues when // running -- wait for the visibility flags to be flipped await until(() => { return elements .map((element, index) => { return (index === 0) === element[$scene].visible; }) .reduce(((l, r) => l && r), true); }); expect(elements[0][$scene].visible).to.be.ok; expect(elements[1][$scene].visible).to.not.be.ok; expect(elements[2][$scene].visible).to.not.be.ok; }); }); }); }); //# sourceMappingURL=model-viewer-base-spec.js.map