Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!DOCTYPE html>
<title>SpeechRecognition availableOnDevice</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
promise_test(async (t) => {
const lang = "en-US";
window.SpeechRecognition = window.SpeechRecognition ||
window.webkitSpeechRecognition;
// Test that it returns a promise.
const resultPromise = SpeechRecognition.availableOnDevice(lang);
assert_true(
resultPromise instanceof Promise,
"availableOnDevice should return a Promise."
);
// Verify the resolved value is a string.
const result = await resultPromise;
assert_true(
typeof result === "string",
"The resolved value of the availableOnDevice promise should be a string."
);
assert_true(
result === "unavailable" || result === "downloadable" ||
result === "downloading" || result === "available",
"The resolved value of the availableOnDevice promise should be a " +
"valid value."
);
}, "SpeechRecognition.availableOnDevice resolves with a string value.");
promise_test(async (t) => {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const frameWindow = iframe.contentWindow;
const frameDOMException = frameWindow.DOMException;
const frameSpeechRecognition =
frameWindow.SpeechRecognition || frameWindow.webkitSpeechRecognition;
iframe.remove();
await promise_rejects_dom(
t,
"InvalidStateError",
frameDOMException,
frameSpeechRecognition.availableOnDevice("en-US"),
);
}, "SpeechRecognition.availableOnDevice rejects in a detached context.");
promise_test(async (t) => {
const iframe = document.createElement("iframe");
// This policy should make the on-device speech recognition
// feature unavailable.
iframe.setAttribute("allow", "on-device-speech-recognition 'none'");
document.body.appendChild(iframe);
t.add_cleanup(() => iframe.remove());
await new Promise(resolve => {
if (iframe.contentWindow &&
iframe.contentWindow.document.readyState === 'complete') {
resolve();
} else {
iframe.onload = resolve;
}
});
const frameWindow = iframe.contentWindow;
const frameSpeechRecognition = frameWindow.SpeechRecognition ||
frameWindow.webkitSpeechRecognition;
assert_true(!!frameSpeechRecognition,
"SpeechRecognition should exist in iframe.");
assert_true(!!frameSpeechRecognition.availableOnDevice,
"availableOnDevice method should exist on SpeechRecognition in iframe.");
// Call availableOnDevice and expect it to resolve to "unavailable".
const availabilityStatus =
await frameSpeechRecognition.availableOnDevice("en-US");
assert_equals(availabilityStatus, "unavailable",
"availableOnDevice should resolve to 'unavailable' if " +
"'on-device-speech-recognition' Permission Policy is 'none'."
);
}, "SpeechRecognition.availableOnDevice resolves to 'unavailable' if " +
"'on-device-speech-recognition' Permission Policy is 'none'.");
promise_test(async (t) => {
const html = `
<!DOCTYPE html>
<script>
window.addEventListener('message', async (event) => {
// Ensure we only process the message intended to trigger the test.
if (event.data !== "runTestCallAvailableOnDevice") return;
try {
const SpeechRecognition = window.SpeechRecognition ||
window.webkitSpeechRecognition;
if (!SpeechRecognition || !SpeechRecognition.availableOnDevice) {
parent.postMessage({
type: "error", // Use "error" for API not found or other issues.
name: "NotSupportedError",
message: "SpeechRecognition.availableOnDevice API not " +
"available in iframe"
}, "*");
return;
}
// Call availableOnDevice and post its resolution.
const availabilityStatus =
await SpeechRecognition.availableOnDevice("en-US");
parent.postMessage(
{ type: "resolution", result: availabilityStatus },
"*"
); // Post the string status
} catch (err) {
// Catch any unexpected errors during the API call or message post.
parent.postMessage({
type: "error",
name: err.name,
message: err.message
}, "*");
}
});
<\/script>
`;
const blob = new Blob([html], { type: "text/html" });
const blobUrl = URL.createObjectURL(blob);
// Important: Revoke the blob URL after the test to free up resources.
t.add_cleanup(() => URL.revokeObjectURL(blobUrl));
const iframe = document.createElement("iframe");
iframe.src = blobUrl;
// Sandboxing with "allow-scripts" is needed for the script inside
// the iframe to run.
// The cross-origin nature is primarily due to the blob URL's origin being
// treated as distinct from the parent page's origin for security
// purposes.
iframe.setAttribute("sandbox", "allow-scripts");
document.body.appendChild(iframe);
t.add_cleanup(() => iframe.remove());
await new Promise(resolve => iframe.onload = resolve);
const testResult = await new Promise((resolve, reject) => {
const timeoutId = t.step_timeout(() => {
reject(new Error("Test timed out waiting for message from iframe. " +
"Ensure iframe script is correctly posting a message."));
}, 6000); // 6-second timeout
window.addEventListener("message", t.step_func((event) => {
// Basic check to ensure the message is from our iframe.
if (event.source !== iframe.contentWindow) return;
clearTimeout(timeoutId);
resolve(event.data);
}));
// Send a distinct message to the iframe to trigger its test logic.
iframe.contentWindow.postMessage("runTestCallAvailableOnDevice", "*");
});
// Check if the iframe's script reported an error (e.g., API not found).
if (testResult.type === "error") {
const errorMessage =
`Iframe reported an error: ${testResult.name} - ` +
testResult.message;
assert_unreached(errorMessage);
}
assert_equals(
testResult.type,
"resolution",
"The call from the iframe should resolve and post a 'resolution' " +
"message."
);
assert_equals(
testResult.result, // Expecting the string "unavailable".
"unavailable",
"availableOnDevice should resolve to 'unavailable' in a cross-origin " +
"iframe."
);
}, "SpeechRecognition.availableOnDevice should resolve to 'unavailable' " +
"in a cross-origin iframe.");
</script>