From ec4bc1ddeb52df2954bb1f19deeb8d0ee697ac23 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Mon, 20 May 2024 15:14:49 -0700 Subject: [PATCH] test: Fix Chromecast testing in lab (#6643) These changes are necessary for compatibility with Chromecast WebDriver Server v2. - Fix a bug in Karma's flat environment support (joeyparrish/karma@9875e98) - Add a test boot file to load CAF on Chromecast devices; required by Chromecast WebDriver Server v2's redirect mode (flat environment at that level) - Also load this cast-boot file in support.html - Rename/reorganize Cast-related externs, which were messy even before the addition of CAF - Remove proxy-cast-platform.js; no longer needed as we move to flatten the test environment - Ignore error events with "null" error; these appear on Linux Chromecasts, only since including CAF - Ignore error events that are actually strings; these appear on Linux Chromecasts, only since including CAF - Disable Fuchsia in the lab until autoplay issues can be resolved --- build/build.py | 11 +- build/check.py | 1 - build/conformance.textproto | 2 - build/shaka-lab.yaml | 2 + externs/cast-namespace.js | 14 +++ externs/{chromecast.js => cast-sdk-v2.js} | 4 - externs/cast-sdk-v3.js | 35 ++++++ externs/mediatailor.js | 0 externs/{cast.js => remote-playback.js} | 4 +- karma.conf.js | 12 +- package-lock.json | 4 +- proxy-cast-platform.js | 138 ---------------------- support.html | 4 +- test/test/boot.js | 30 ++++- test/test/cast-boot.js | 37 ++++++ 15 files changed, 134 insertions(+), 164 deletions(-) create mode 100644 externs/cast-namespace.js rename externs/{chromecast.js => cast-sdk-v2.js} (99%) create mode 100644 externs/cast-sdk-v3.js mode change 100755 => 100644 externs/mediatailor.js rename externs/{cast.js => remote-playback.js} (69%) delete mode 100644 proxy-cast-platform.js create mode 100644 test/test/cast-boot.js diff --git a/build/build.py b/build/build.py index 9894421f9..a895e5d2e 100755 --- a/build/build.py +++ b/build/build.py @@ -45,6 +45,7 @@ import argparse import logging import os import re +import shutil import compiler import generateLocalizations @@ -302,11 +303,12 @@ class Build(object): if not closure.compile(closure_opts, force): return False + source_base = shakaBuildHelpers.get_source_base() + # Don't pass local node modules to the extern generator. But don't simply # exclude the string 'node_modules', either, since Shaka Player could be # rebuilt after installing it as a node module. - node_modules_path = os.path.join( - shakaBuildHelpers.get_source_base(), 'node_modules') + node_modules_path = os.path.join(source_base, 'node_modules') local_include = set([f for f in self.include if node_modules_path not in f]) extern_generator = compiler.ExternGenerator(local_include, build_name) @@ -323,6 +325,11 @@ class Build(object): if not ts_def_generator.generate(force): return False + # Copy this file to dist/ where support.html can use it + shutil.copy( + os.path.join(source_base, 'test', 'test', 'cast-boot.js'), + os.path.join(source_base, 'dist', 'cast-boot.js')) + return True diff --git a/build/check.py b/build/check.py index a5b692f36..1f6799ddb 100755 --- a/build/check.py +++ b/build/check.py @@ -283,7 +283,6 @@ def check_tests(args): [closure_base_js])) files.add(os.path.join(base, 'demo', 'common', 'asset.js')) files.add(os.path.join(base, 'demo', 'common', 'assets.js')) - files.add(os.path.join(base, 'proxy-cast-platform.js')) localizations = compiler.GenerateLocalizations(None) localizations.generate(args.force) diff --git a/build/conformance.textproto b/build/conformance.textproto index 510fc22ca..99dc11dc8 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -73,7 +73,6 @@ requirement { whitelist_regexp: "demo/" whitelist_regexp: "test/" whitelist_regexp: "node_modules/" - whitelist_regexp: "proxy-cast-platform.js" # This global variable is generated by Google-internal tooling, and should be # allowed. It will not end up in the compiled code, only at an intermediate @@ -287,7 +286,6 @@ requirement: { "shaka.util.Timer instead." whitelist_regexp: "demo/" whitelist_regexp: "test/" - whitelist_regexp: "proxy-cast-platform.js" } # Disallow eval, except when testing for modern JS syntax in demo diff --git a/build/shaka-lab.yaml b/build/shaka-lab.yaml index 71b2cfdca..95ddbafeb 100644 --- a/build/shaka-lab.yaml +++ b/build/shaka-lab.yaml @@ -241,6 +241,8 @@ ChromecastGTV: ChromecastHub: browser: chromecast version: hub + # Disabled by default until we resolve issues with autoplay on Fuchsia. + disabled: true ChromecastSpeaker: # This is a headless device, and our tests are not yet known to work here. diff --git a/externs/cast-namespace.js b/externs/cast-namespace.js new file mode 100644 index 000000000..b0943b7cc --- /dev/null +++ b/externs/cast-namespace.js @@ -0,0 +1,14 @@ +/*! @license + * Shaka Player + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Google Cast namespace definitions, shared by other externs + * files. + * @externs + */ + +/** @const */ +var cast = {}; diff --git a/externs/chromecast.js b/externs/cast-sdk-v2.js similarity index 99% rename from externs/chromecast.js rename to externs/cast-sdk-v2.js index 382165769..b6b2f8658 100644 --- a/externs/chromecast.js +++ b/externs/cast-sdk-v2.js @@ -15,10 +15,6 @@ var __onGCastApiAvailable; -/** @const */ -var cast = {}; - - /** @const */ cast.receiver = {}; diff --git a/externs/cast-sdk-v3.js b/externs/cast-sdk-v3.js new file mode 100644 index 000000000..cc74b9cc6 --- /dev/null +++ b/externs/cast-sdk-v3.js @@ -0,0 +1,35 @@ +/*! @license + * Shaka Player + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Externs for the limited subset of the Cast Application + * Framework (Receiver SDK v3) that we use in our test infrastructure. + * + * @externs + */ + +/** @const */ +cast.framework = {}; + +/** + * @typedef {{ + * statusText: string, + * disableIdleTimeout: boolean, + * skipPlayersLoad: boolean + * }} + */ +cast.framework.CastReceiverOptions; + +cast.framework.CastReceiverContext = class { + /** @return {!cast.framework.CastReceiverContext} */ + static getInstance() {} + + /** + * @param {!cast.framework.CastReceiverOptions} options + * @return {!cast.framework.CastReceiverContext} + */ + start(options) {} +}; diff --git a/externs/mediatailor.js b/externs/mediatailor.js old mode 100755 new mode 100644 diff --git a/externs/cast.js b/externs/remote-playback.js similarity index 69% rename from externs/cast.js rename to externs/remote-playback.js index 7ae8fd229..b4d8107a8 100644 --- a/externs/cast.js +++ b/externs/remote-playback.js @@ -5,8 +5,8 @@ */ /** - * @fileoverview Externs for HTMLMediaElement related to casting which were - * missing in the Closure compiler. + * @fileoverview Externs for HTMLMediaElement related to remote playback which + * were missing in the Closure compiler. * * @externs */ diff --git a/karma.conf.js b/karma.conf.js index c523938be..1655c97ad 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -197,7 +197,13 @@ module.exports = (config) => { // list of files / patterns to load in the browser files: [ - // Polyfills first, primarily for IE 11 and older TVs: + // The Cast boot file must come first, to start the SDK and respond as + // quickly as possible to the Cast platform. Without this up front, we + // tend to see the Chromecast time out and shut down the receiver that + // hosts our tests. + 'test/test/cast-boot.js', + + // Polyfills before anything else, primarily for older TVs: // Promise polyfill, required since we test uncompiled code on IE11 'node_modules/es6-promise-polyfill/promise.js', // Babel polyfill, required for async/await @@ -230,9 +236,6 @@ module.exports = (config) => { // test utilities next, which fill in that namespace 'test/test/util/*.js', - // Proxy cast.__platform__ methods across frames, necessary in testing - 'proxy-cast-platform.js', - // bootstrapping for the test suite last; this will load the actual tests 'test/test/boot.js', @@ -389,7 +392,6 @@ module.exports = (config) => { 'ui/**/*.js': ['babel', 'sourcemap'], 'test/**/*.js': ['babel', 'sourcemap'], 'third_party/**/*.js': ['babel', 'sourcemap'], - 'proxy-cast-platform.js': ['babel', 'sourcemap'], }, babelPreprocessor: { diff --git a/package-lock.json b/package-lock.json index 99a29380d..8b2bf634a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5537,7 +5537,7 @@ }, "node_modules/karma": { "version": "6.4.3", - "resolved": "git+ssh://git@github.com/joeyparrish/karma.git#d98765e43fe801b35452fdb6cab3d1d0bac519a2", + "resolved": "git+ssh://git@github.com/joeyparrish/karma.git#9875e9898b999684c2b2767c34d766ab2220f351", "dev": true, "license": "MIT", "dependencies": { @@ -12947,7 +12947,7 @@ } }, "karma": { - "version": "git+ssh://git@github.com/joeyparrish/karma.git#d98765e43fe801b35452fdb6cab3d1d0bac519a2", + "version": "git+ssh://git@github.com/joeyparrish/karma.git#9875e9898b999684c2b2767c34d766ab2220f351", "dev": true, "from": "karma@github:joeyparrish/karma#shaka-fixes", "requires": { diff --git a/proxy-cast-platform.js b/proxy-cast-platform.js deleted file mode 100644 index 5aeea0782..000000000 --- a/proxy-cast-platform.js +++ /dev/null @@ -1,138 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Proxy cast platform methods across frames. Shared between the - * test environment and support.html. - */ - -/** - * Patch Cast's cast.__platform__.canDisplayType to allow it to operate across - * frames and origins in our testing environment. - * - * The Cast runtime only exposes cast.__platform__ on the top window, not - * iframes embedded within it. However, both Chromecast WebDriver Server in - * our lab and the test runner Karma use iframes, and there are three different - * origins involved: WebDriver Server's receiver at github.io, Karma's - * top-level, and an inner frame of Karma that is raw HTML as text with no - * origin. - * - * With all of these complexities, the only way to access cast.__platform__ is - * asynchronously via postMessage. This means all callers of - * cast.__platform__.canDisplayType must use `await`, even though the - * underlying method is synchronous. If the caller is running in the top frame - * (as in a real receiver), `await` will do no harm. If the caller is running - * inside our tests in Karma, the `await` is critical to access __platform__ - * via this shim. - */ -function proxyCastCanDisplayType() { - if (!navigator.userAgent.includes('CrKey')) { - // Not Chromecast, do nothing. - return; - } - - // Create the namespaces if needed. - if (!window.cast) { - window['cast'] = {}; - } - if (!cast.__platform__) { - cast['__platform__'] = {}; - } - - if (cast.__platform__.canDisplayType) { - // Already exists, do nothing. - return; - } - - // Create an async shim. Calls to canDisplayType will be translated into - // async messages to the top frame, which will then execute the method and - // post a message back with results (or an error). The resolve/reject - // functions for the shim's returned Promise will be stored temporarily in - // these maps and matched up by request ID. - /** @type {!Map} */ - const resolveMap = new Map(); - /** @type {!Map} */ - const rejectMap = new Map(); - /** @type {number} */ - let nextId = 0; - - /** - * @typedef {{ - * id: number, - * type: string, - * result: *, - * }} - */ - let CastShimMessage; - - // Listen for message events for results/errors from the top frame. - window.addEventListener('message', (event) => { - const data = /** @type {CastShimMessage} */(event['data']); - console.log('Received cross-frame message', data); - - if (data.type == 'cast.__platform__:result') { - // Find the matching resolve function and resolve the promise for this - // request. - const resolve = resolveMap.get(data.id); - if (resolve) { - resolve(data.result); - - // Clear both resolve and reject from the maps for this ID. - resolveMap.delete(data.id); - rejectMap.delete(data.id); - } - } else if (data.type == 'cast.__platform__:error') { - // Find the matching reject function and reject the promise for this - // request. - const reject = rejectMap.get(data.id); - if (reject) { - reject(data.result); - - // Clear both resolve and reject from the maps for this ID. - resolveMap.delete(data.id); - rejectMap.delete(data.id); - } - } - }); - - // Shim canDisplayType to proxy the request up to the top frame. - cast.__platform__.canDisplayType = /** @type {?} */(castCanDisplayTypeShim); - - /** - * @param {string} type - * @return {!Promise} - */ - function castCanDisplayTypeShim(type) { - return new Promise((resolve, reject) => { - // Craft a message for the top frame to execute this method for us. - const message = { - id: nextId++, - type: 'cast.__platform__', - command: 'canDisplayType', - args: Array.from(arguments), - }; - - // Store the resolve and reject functions so we can act on results/errors - // later. - resolveMap.set(message.id, resolve); - rejectMap.set(message.id, reject); - - // Reject after a 5s timeout. This can happen if we're running under an - // incompatible version of Chromecast WebDriver Server's receiver app. - setTimeout(() => { - reject(new Error('canDisplayType timeout!')); - - // Clear both resolve and reject from the maps for this ID. - resolveMap.delete(message.id); - rejectMap.delete(message.id); - }, 5000); - - // Send the message to the top frame. - console.log('Sending cross-frame message', message); - window.top.postMessage(message, '*'); - }); - } -} diff --git a/support.html b/support.html index 29e5641e9..029ac34b6 100644 --- a/support.html +++ b/support.html @@ -38,9 +38,10 @@ } + -