Files
sousa-gecko/testing/web-platform/tests/digital-credentials/create.tentative.https.html
T
Marcos Cáceres ddad9b3850 Bug 2042954 [wpt PR 58420] - Digital credentials: prepare algorithm tests, a=testonly
Automatic update from web-platform-tests
Digital credentials: prepare algorithm tests (#58420)

* Digital credentials: prepare algorithm tests

* Add some helpers

* Apply suggestion from @mohamedamir

Co-authored-by: Mohamed Amir Yosef <mamir@chromium.org>

* Apply suggestions from code review

Co-authored-by: Marcos Cáceres <marcos@marcosc.com>

* Update spec URL from WICG to w3c-fedid

* Split protocol filtering tests by protocol family

Rename protocol-filtering.https.html to protocol-filtering-mdoc.https.html
and add protocol-filtering-openid4vp.https.html with coverage for all three
openid4vp sub-protocols (unsigned, signed, multisigned) plus near-miss
variants that should be filtered out.

Addresses review feedback from @mohamedamir requesting openid4vp filtering
test coverage.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Assert user activation before second concurrent request

Adds assert_true(navigator.userActivation.isActive) after the second
bless call to confirm the rejection is due to the pending request,
not missing activation.

* Fix concurrent-requests test stability

Use bless's action callback to dispatch the second get() call within
the same microtask as the activation grant. This eliminates the event
loop yield between the two calls, preventing the first request's IPC
response from clearing the pending state in the browser process before
the second request arrives.

* Digital credentials: prepare algorithm tests

* Update spec URL from WICG to w3c-fedid

* Use makeCanonicalCreateRequest() for openid4vci in create filtering tests

The protocol-filtering tests in create.tentative.https.html hardcoded
{ protocol: "openid4vci", data: {} } while importing makeCanonicalCreateRequest
without using it. Route the valid-protocol requests through the helper so the
import is used and the requests pick up the canonical openid4vci object once it
is defined. No behavior change today: the canonical openid4vci object is
currently empty, so the helper returns the same request object.

* Remove redundant protocol-filtering.https.html

protocol-filtering.https.html duplicated protocol-filtering-mdoc.https.html:
the seven invalid-protocol subtests were identical, and its one unique "valid"
subtest was mislabeled "org-iso-mdoc" while actually sending the default set of
both openid4vp-v1-unsigned and org-iso-mdoc. The org-iso-mdoc and openid4vp
filtering cases are already covered by protocol-filtering-mdoc.https.html and
protocol-filtering-openid4vp.https.html.

---------

Co-authored-by: Mohamed Amir Yosef <mamir@chromium.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
--

wpt-commits: fa3b32ced60d1099cd9dc4757ef43ce7e3df55ec
wpt-pr: 58420
2026-06-25 17:01:41 +00:00

362 lines
11 KiB
HTML

<!DOCTYPE html>
<meta charset="utf-8">
<title>Digital Credential API tests for create.</title>
<link rel="help" href="https://w3c-fedid.github.io/digital-credentials/" />
<script src="/common/get-host-info.sub.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<body>
<iframe id="cross-origin" allow="digital-credentials-create"></iframe>
<iframe id="same-origin"></iframe>
</body>
<script type="module">
import { makeCreateOptions, makeCanonicalCreateRequest, sendMessage, loadIframe } from "./support/helper.js";
const iframeSameOrigin = document.querySelector("iframe#same-origin");
const iframeCrossOrigin = document.querySelector("iframe#cross-origin");
promise_setup(async () => {
const hostInfo = get_host_info();
await Promise.all([
loadIframe(
iframeCrossOrigin,
`${hostInfo.HTTPS_REMOTE_ORIGIN}/digital-credentials/support/iframe.html`
),
loadIframe(iframeSameOrigin, "/digital-credentials/support/iframe.html"),
]);
});
promise_test(async (t) => {
iframeSameOrigin.focus();
for (const global of [window, iframeSameOrigin.contentWindow]) {
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create()
);
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({})
);
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({ x: "y" })
);
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({ x: "y", y: "z" })
);
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({ mediation: "required" })
);
const abortController = new AbortController();
const { signal } = abortController;
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({ signal })
);
await promise_rejects_dom(
t,
"NotSupportedError",
global.DOMException,
global.navigator.credentials.create({ signal, mediation: "required" })
);
}
}, "Calling navigator.credentials.create() without a digital member same origin.");
promise_test(async (t) => {
for (const r of [undefined, []]) {
const options = {
digital: {
requests: r
},
};
await promise_rejects_js(t, TypeError, navigator.credentials.create(options));
}
}, "navigator.credentials.create() API rejects if there are no credential request.");
promise_test(async (t) => {
iframeSameOrigin.focus();
const { contentWindow: iframeWindow } = iframeSameOrigin;
for (const r of [undefined, []]) {
const options = {
digital: {
requests: r
},
};
await test_driver.bless("user activation");
await promise_rejects_js(
t,
iframeWindow.TypeError,
iframeWindow.navigator.credentials.create(options)
);
}
}, "navigator.credentials.create() API rejects if there are no credential request for same-origin iframe.");
promise_test(async (t) => {
iframeCrossOrigin.focus();
for (const r of [undefined, []]) {
const options = {
digital: {
requests: r
},
};
const result = await sendMessage(iframeCrossOrigin, {
action: "create",
options,
});
assert_equals(result.constructor, "TypeError");
}
}, "navigator.credentials.create() API rejects if there are no credential request in cross-origin iframe.");
promise_test(async (t) => {
const abortController = new AbortController();
const { signal } = abortController;
abortController.abort();
for (const options of [{ signal }, makeCreateOptions({protocol: [], signal})]) {
await promise_rejects_dom(
t,
"AbortError",
navigator.credentials.create(options)
);
}
}, "navigator.credentials.create() promise is rejected if called with an aborted controller.");
promise_test(async (t) => {
iframeSameOrigin.focus();
const { contentWindow: iframeWindow } = iframeSameOrigin;
const abortController = new iframeWindow.AbortController();
const { signal } = abortController;
abortController.abort();
for (const options of [{ signal }, makeCreateOptions({protocol: [], signal})]) {
await test_driver.bless("user activation");
await promise_rejects_dom(
t,
"AbortError",
iframeWindow.DOMException,
iframeWindow.navigator.credentials.create(options)
);
assert_true(
navigator.userActivation.isActive,
"User activation is still active."
);
}
}, "navigator.credentials.create() promise is rejected if called with an aborted controller in same-origin iframe.");
promise_test(async (t) => {
iframeCrossOrigin.focus();
for (const options of [undefined, {}, makeCreateOptions({protocol: []})]) {
const result = await sendMessage(iframeCrossOrigin, {
abort: "before",
action: "create",
options,
});
assert_equals(result.constructor, "DOMException");
assert_equals(result.name, "AbortError");
}
}, "navigator.credentials.create() promise is rejected if called with an aborted signal in cross-origin iframe.");
promise_test(async (t) => {
const abortController = new AbortController();
const { signal } = abortController;
const options = makeCreateOptions({ signal });
await test_driver.bless("user activation");
const promise = promise_rejects_dom(
t,
"AbortError",
DOMException,
navigator.credentials.create(options)
);
abortController.abort();
await promise;
}, "navigator.credentials.create() promise is rejected if abort controller is aborted after call to create().");
promise_test(async (t) => {
iframeCrossOrigin.focus();
const result = await sendMessage(iframeCrossOrigin, {
abort: "after",
action: "create",
needsActivation: true,
options: makeCreateOptions(),
});
assert_equals(result.constructor, "DOMException");
assert_equals(result.name, "AbortError");
}, "navigator.credentials.create() promise is rejected if abort controller is aborted after call to create() in cross-origin iframe.");
promise_test(async (t) => {
/** @type sequence<CredentialMediationRequirement> */
const mediations = ["silent", "optional", "conditional", "required"];
const abortController = new AbortController();
const { signal } = abortController;
abortController.abort();
for (const mediation of mediations) {
const requestPromise = navigator.credentials.create({
mediation,
signal,
});
await promise_rejects_dom(t, "AbortError", requestPromise);
}
}, "Mediation is implicitly required and hence ignored. Request is aborted regardless.");
promise_test(async (t) => {
await promise_rejects_js(
t,
TypeError,
navigator.credentials.create({digital: {}}));
}, "`requests` field is required in the options object.");
promise_test(async (t) => {
const throwingValues = [
{ val: BigInt(123) },
(() => { const o = {}; o.self = o; return o; })(),
{ toJSON() { throw new TypeError("toJSON throws"); } }
];
for (const badValue of throwingValues) {
const options = makeCreateOptions({ data: badValue });
await test_driver.bless("user activation");
await promise_rejects_js(
t,
TypeError,
navigator.credentials.create(options),
`Should throw for: ${String(badValue)}`
);
}
}, "Throws TypeError when request data is not JSON stringifiable.");
promise_test(async (t) => {
const options = {
password: document.createElement("form"),
};
await promise_rejects_js(
t,
TypeError,
navigator.credentials.create(options),
"Should throw for invalid form element"
);
}, "Throws when form element does not contain password.");
promise_test(async (t) => {
const controller = new AbortController();
const signal = controller.signal;
const options = {
digital: {
requests: [
{
protocol: "unknown-protocol",
data: { val: BigInt(1) }, // malformed/non-serializable
},
{
protocol: "openid4vci",
data: { some: "data" }, // proper data
},
],
},
signal,
};
await test_driver.bless("user activation");
const promise = navigator.credentials.create(options);
controller.abort();
await promise_rejects_dom(t, "AbortError", promise);
}, "navigator.credentials.create() with one unknown protocol with malformed data and one known protocol with proper data should skip the unknown one and not fail.");
promise_test(async (t) => {
const options = {
digital: {
requests: [{ protocol: "unknown-protocol", data: {} }]
}
};
await promise_rejects_js(
t,
TypeError,
navigator.credentials.create(options)
);
}, "All unsupported protocols filtered results in TypeError.");
promise_test(async (t) => {
const options = {
digital: {
requests: [
{ protocol: "fake-protocol-1", data: {} },
{ protocol: "fake-protocol-2", data: {} },
{ protocol: "another-unknown", data: {} }
]
}
};
await promise_rejects_js(
t,
TypeError,
navigator.credentials.create(options)
);
}, "Multiple unsupported protocols all filtered results in TypeError.");
promise_test(async (t) => {
const abortController = new AbortController();
const { signal } = abortController;
const options = {
digital: {
requests: [makeCanonicalCreateRequest("openid4vci")]
},
signal
};
await test_driver.bless("user activation");
const promise = navigator.credentials.create(options);
abortController.abort();
await promise_rejects_dom(
t,
"AbortError",
promise,
"Valid 'openid4vci' protocol should not be filtered out"
);
}, "Valid 'openid4vci' protocol passes through filtering.");
promise_test(async (t) => {
const abortController = new AbortController();
const { signal } = abortController;
const options = {
digital: {
requests: [
{ protocol: "unknown-protocol", data: {} },
makeCanonicalCreateRequest("openid4vci"),
{ protocol: "another-fake", data: {} }
]
},
signal
};
await test_driver.bless("user activation");
const promise = navigator.credentials.create(options);
abortController.abort();
await promise_rejects_dom(
t,
"AbortError",
promise,
"Valid protocol among invalid ones should survive filtering"
);
}, "Mix of valid and invalid protocols - valid survives filtering.");
</script>