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
This commit is contained in:
Joey Parrish
2024-05-20 15:14:49 -07:00
committed by GitHub
parent 0a68e934fd
commit ec4bc1ddeb
15 changed files with 134 additions and 164 deletions
+9 -2
View File
@@ -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
-1
View File
@@ -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)
-2
View File
@@ -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
+2
View File
@@ -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.
+14
View File
@@ -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 = {};
@@ -15,10 +15,6 @@
var __onGCastApiAvailable;
/** @const */
var cast = {};
/** @const */
cast.receiver = {};
+35
View File
@@ -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) {}
};
Executable → Regular
View File
@@ -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
*/
+7 -5
View File
@@ -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: {
+2 -2
View File
@@ -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": {
-138
View File
@@ -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<number, function(?)>} */
const resolveMap = new Map();
/** @type {!Map<number, function(?)>} */
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<boolean>}
*/
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, '*');
});
}
}
+2 -2
View File
@@ -38,9 +38,10 @@
}
</style>
<script src="dist/cast-boot.js"></script>
<script src="dist/shaka-player.compiled.js"></script>
<script src="proxy-cast-platform.js"></script>
<script>
function whenLoaded(fn) {
// IE 9 fires DOMContentLoaded, and enters the "interactive"
// readyState, before document.body has been initialized, so wait
@@ -74,7 +75,6 @@
}
function doTest() {
proxyCastCanDisplayType();
shaka.polyfill.installAll();
if (shaka.Player.isBrowserSupported()) {
+24 -6
View File
@@ -97,8 +97,29 @@ function failTestsOnUnhandledErrors() {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
window.addEventListener('error', (event) => {
if (typeof event == 'string') {
// Since we moved to a flat (frameless) testing environment, we sometimes
// get error "events" on Chromecast where we only get the string "Script
// error." instead of an actual event object. This would be consistent
// with an unhandled error thrown by a cross-origin script (such as the
// Cast SDK), but this string-only form should only be sent to the
// onerror callback and not the 'error' event listener. This is both a
// bug in the SDK (unhandled error) and in the platform (called event
// listener with wrong format). We ignore it here.
console.log('Suppressing error event with only string:', event);
return;
}
/** @type {?} */
const error = event['error'];
if (!error) {
// Since we moved to a flat (frameless) testing environment, we sometimes
// get error events on Chromecast where the error field is null. It's not
// clear why. Ignore these.
console.log('Suppressing error event with no error:', event);
return;
}
failOnError('Unhandled error', error);
});
}
@@ -129,9 +150,9 @@ function disableScrollbars() {
// eslint-disable-next-line no-restricted-syntax
} catch (error) {
// On some platforms (Chromecast, Tizen), we are prevented from accessing
// the host context, even though it should be in the same origin. Ignore
// errors here, so that the rest of the critical boot sequence can complete.
// On some platforms (Tizen), we are prevented from accessing the host
// context, even though it should be in the same origin. Ignore errors
// here, so that the rest of the critical boot sequence can complete.
}
}
@@ -414,9 +435,6 @@ function setupTestEnvironment() {
disableScrollbars();
workAroundLegacyEdgePromiseIssues();
// Defined in proxy-cast-platform.js:
proxyCastCanDisplayType();
// The spec filter callback occurs before calls to beforeAll, so we need to
// install polyfills here to ensure that browser support is correctly
// detected.
+37
View File
@@ -0,0 +1,37 @@
/*! @license
* Shaka Player
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Use indexOf() here, normally banned in favor of includes(), for
// compatibility with the oldest devices and runtimes.
// eslint-disable-next-line no-restricted-syntax
if (navigator.userAgent.indexOf('CrKey') != -1) {
// Running in any frame of a Chromecast device.
// Load and activate the Cast SDK.
console.log('Loading Cast SDK');
const script =
/** @type {HTMLScriptElement} **/(document.createElement('script'));
script.src = 'https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js';
// Use an anonymous function here, normally banned in favor of arrow
// functions, for compatibility with the oldest devices and runtimes.
// eslint-disable-next-line no-restricted-syntax
script.onload = function() {
cast.framework.CastReceiverContext.getInstance().start({
// What to show as the status of the app to senders
statusText: 'Shaka Player Testing',
// Don't shut down for a lack of sender input
disableIdleTimeout: true,
// Don't load player libraries, since we are loading Shaka here directly
skipPlayersLoad: true,
});
console.log('Cast SDK loaded');
};
document.head.appendChild(script);
}