mirror of
https://github.com/lexmount/moli.git
synced 2026-11-04 16:06:44 +00:00
feat(payment): implement native payment update event values
This commit is contained in:
@@ -127,6 +127,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
|
||||
ConstructorKind::ToolCancelEvent => {
|
||||
build_value_event_template(scope, ValueEventKind::ToolCancel)
|
||||
}
|
||||
ConstructorKind::PaymentRequestUpdateEvent => {
|
||||
build_value_event_template(scope, ValueEventKind::PaymentRequestUpdate)
|
||||
}
|
||||
ConstructorKind::PaymentMethodChangeEvent => {
|
||||
build_value_event_template(scope, ValueEventKind::PaymentMethodChange)
|
||||
}
|
||||
ConstructorKind::UiEvent => {
|
||||
build_event_subclass_template(scope, EventSubclassKind::UiEvent)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ const TOOL_NAME_SLOT: &str = "__moliToolEventName";
|
||||
const GAMEPAD_SLOT: &str = "__moliValueEventGamepad";
|
||||
const PICTURE_IN_PICTURE_WINDOW_SLOT: &str = "__moliPictureInPictureEventWindow";
|
||||
|
||||
const PAYMENT_METHOD_NAME_SLOT: &str = "__moliPaymentMethodChangeEventMethodName";
|
||||
const PAYMENT_METHOD_DETAILS_SLOT: &str = "__moliPaymentMethodChangeEventMethodDetails";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(in crate::context_bootstrap) enum ValueEventKind {
|
||||
Animation,
|
||||
@@ -31,6 +34,8 @@ pub(in crate::context_bootstrap) enum ValueEventKind {
|
||||
ToolCancel,
|
||||
Gamepad,
|
||||
PictureInPicture,
|
||||
PaymentRequestUpdate,
|
||||
PaymentMethodChange,
|
||||
}
|
||||
|
||||
impl ValueEventKind {
|
||||
@@ -44,6 +49,8 @@ impl ValueEventKind {
|
||||
Self::ToolCancel => "ToolCancelEvent",
|
||||
Self::Gamepad => "GamepadEvent",
|
||||
Self::PictureInPicture => "PictureInPictureEvent",
|
||||
Self::PaymentRequestUpdate => "PaymentRequestUpdateEvent",
|
||||
Self::PaymentMethodChange => "PaymentMethodChangeEvent",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +226,59 @@ struct PictureInPictureEventInit<'s> {
|
||||
picture_in_picture_window: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::PaymentMethodChangeEvent, enumerable, receiver)]
|
||||
struct PaymentMethodChangeEventPrototypeDeclaration {
|
||||
#[webapi(accessor_property = "methodName", getter = payload_getter, data = v8str(scope, PAYMENT_METHOD_NAME_SLOT))]
|
||||
method_name: (),
|
||||
#[webapi(accessor_property = "methodDetails", getter = payload_getter, data = v8str(scope, PAYMENT_METHOD_DETAILS_SLOT))]
|
||||
method_details: (),
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "PaymentMethodChangeEventInit")]
|
||||
struct PaymentMethodChangeEventInit<'s> {
|
||||
#[webidl(inherit)]
|
||||
base: EventInit,
|
||||
#[webidl(nullable)]
|
||||
method_details: Option<v8::Local<'s, v8::Object>>,
|
||||
#[webidl(with = string_member)]
|
||||
method_name: v8::Local<'s, v8::String>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::PaymentRequestUpdateEvent, enumerable, receiver)]
|
||||
struct PaymentRequestUpdateEventPrototypeDeclaration {
|
||||
#[webapi(method, length = 1, callback = payment_update_with_callback)]
|
||||
update_with: (),
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "PaymentRequestUpdateEvent.updateWith")]
|
||||
struct PaymentUpdateWithArgs<'s> {
|
||||
#[webidl(required)]
|
||||
_details_promise: v8::Local<'s, v8::Promise>,
|
||||
}
|
||||
|
||||
fn payment_update_with_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if webidl::parse_args::<PaymentUpdateWithArgs>(scope, &args).is_none() {
|
||||
return;
|
||||
}
|
||||
// Only author-created payment events are currently produced. They must
|
||||
// reject updates even during dispatch, after Web IDL Promise conversion.
|
||||
// Trusted payment events require an interactive PaymentRequest backend.
|
||||
crate::native_bridge::throw_dom_exception(
|
||||
scope,
|
||||
"InvalidStateError",
|
||||
11,
|
||||
"updateWith requires a trusted event associated with an interactive payment request",
|
||||
);
|
||||
}
|
||||
|
||||
fn string_member<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
@@ -265,6 +325,16 @@ pub(in crate::context_bootstrap) fn install_value_event_template_bindings<'s>(
|
||||
"GamepadEvent" => {
|
||||
GamepadEventPrototypeDeclaration::initialize_prototype_template(scope, prototype)
|
||||
}
|
||||
"PaymentRequestUpdateEvent" => {
|
||||
PaymentRequestUpdateEventPrototypeDeclaration::initialize_prototype_template(
|
||||
scope, prototype,
|
||||
)
|
||||
}
|
||||
"PaymentMethodChangeEvent" => {
|
||||
PaymentMethodChangeEventPrototypeDeclaration::initialize_prototype_template(
|
||||
scope, prototype,
|
||||
)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +376,8 @@ fn value_event_constructor<'s>(
|
||||
Some(5) => ValueEventKind::ToolCancel,
|
||||
Some(6) => ValueEventKind::Gamepad,
|
||||
Some(7) => ValueEventKind::PictureInPicture,
|
||||
Some(8) => ValueEventKind::PaymentRequestUpdate,
|
||||
Some(9) => ValueEventKind::PaymentMethodChange,
|
||||
_ => return,
|
||||
};
|
||||
if !args.is_construct_call() {
|
||||
@@ -427,6 +499,32 @@ fn value_event_constructor<'s>(
|
||||
parsed.base.composed,
|
||||
)
|
||||
}
|
||||
ValueEventKind::PaymentRequestUpdate => {
|
||||
// PaymentRequestUpdateEventInit adds no members to EventInit.
|
||||
let parsed = webidl::parse_dictionary_object::<EventInit>(scope, dictionary)?;
|
||||
(parsed.bubbles, parsed.cancelable, parsed.composed)
|
||||
}
|
||||
ValueEventKind::PaymentMethodChange => {
|
||||
let parsed = webidl::parse_dictionary_object::<PaymentMethodChangeEventInit>(
|
||||
scope, dictionary,
|
||||
)?;
|
||||
let details = parsed
|
||||
.method_details
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| v8::null(scope).into());
|
||||
set_event_private_value(scope, state, PAYMENT_METHOD_DETAILS_SLOT, details);
|
||||
set_event_private_value(
|
||||
scope,
|
||||
state,
|
||||
PAYMENT_METHOD_NAME_SLOT,
|
||||
parsed.method_name.into(),
|
||||
);
|
||||
(
|
||||
parsed.base.bubbles,
|
||||
parsed.base.cancelable,
|
||||
parsed.base.composed,
|
||||
)
|
||||
}
|
||||
ValueEventKind::Gamepad => {
|
||||
let parsed =
|
||||
webidl::parse_dictionary_object::<GamepadEventInit>(scope, dictionary)?;
|
||||
|
||||
@@ -107,6 +107,8 @@ pub(in crate::context_bootstrap) const WORKER_SHARED_INTERFACE_NAMES: &[&str] =
|
||||
];
|
||||
|
||||
const SECURE_CONTEXT_ONLY_INTERFACE_NAMES: &[&str] = &[
|
||||
"PaymentRequestUpdateEvent",
|
||||
"PaymentMethodChangeEvent",
|
||||
"AuthenticatorAssertionResponse",
|
||||
"AuthenticatorAttestationResponse",
|
||||
"AuthenticatorResponse",
|
||||
|
||||
@@ -4,6 +4,14 @@ use crate::web_api_interfaces;
|
||||
use std::collections::HashSet;
|
||||
|
||||
const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::PaymentRequestUpdateEvent::DESCRIPTOR,
|
||||
kind: ConstructorKind::PaymentRequestUpdateEvent,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::PaymentMethodChangeEvent::DESCRIPTOR,
|
||||
kind: ConstructorKind::PaymentMethodChangeEvent,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::PictureInPictureEvent::DESCRIPTOR,
|
||||
kind: ConstructorKind::PictureInPictureEvent,
|
||||
|
||||
@@ -15,6 +15,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
|
||||
ToolCancelEvent,
|
||||
GamepadEvent,
|
||||
PictureInPictureEvent,
|
||||
PaymentRequestUpdateEvent,
|
||||
PaymentMethodChangeEvent,
|
||||
UiEvent,
|
||||
FocusEvent,
|
||||
CompositionEvent,
|
||||
|
||||
@@ -2246,3 +2246,5 @@ mod dom_rect_structured_clone;
|
||||
mod domrect_receiver_consolidation;
|
||||
|
||||
mod resize_observer_entries;
|
||||
|
||||
mod payment_events;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
(async function paymentEventChecks(realms) {
|
||||
const checks = [];
|
||||
const check = (name, passed) => checks.push({name, passed: Boolean(passed)});
|
||||
const throws = (name, action, Constructor, expectedName) => {
|
||||
let error;
|
||||
try { action(); } catch (caught) { error = caught; }
|
||||
check(name, error instanceof Constructor && (!expectedName || error.name === expectedName));
|
||||
};
|
||||
for (let ownerIndex = 0; ownerIndex < realms.length; ownerIndex++) {
|
||||
const owner = realms[ownerIndex];
|
||||
for (const name of ['PaymentRequestUpdateEvent', 'PaymentMethodChangeEvent']) {
|
||||
const C = owner[name], p = C.prototype;
|
||||
const prefix = ownerIndex + ':' + name + ':';
|
||||
check(prefix + 'constructor metadata', C.length === 1 && C.name === name);
|
||||
const empty = new C('test');
|
||||
check(prefix + 'Event inheritance', empty instanceof owner.Event && empty instanceof owner.PaymentRequestUpdateEvent);
|
||||
check(prefix + 'default event header', empty.type === 'test' && !empty.bubbles && !empty.cancelable && !empty.composed && !empty.isTrusted && empty.target === null);
|
||||
for (const init of [undefined, null, {}]) {
|
||||
const event = new C('test', init);
|
||||
check(prefix + 'empty dictionary ' + String(init), !event.bubbles && !event.cancelable && !event.composed);
|
||||
if (name === 'PaymentMethodChangeEvent') check(prefix + 'payload defaults ' + String(init), event.methodName === '' && event.methodDetails === null);
|
||||
}
|
||||
throws(prefix + 'requires new', () => C('test'), owner.TypeError);
|
||||
throws(prefix + 'requires type', () => new C(), owner.TypeError);
|
||||
const order = [];
|
||||
const details = new Proxy({}, {get() {throw Error('payload object must not be read');}});
|
||||
const init = {get bubbles() {order.push('bubbles');return true;}, get cancelable() {order.push('cancelable');return true;}, get composed() {order.push('composed');return true;}, get methodDetails() {order.push('methodDetails');return details;}, get methodName() {order.push('methodName');return {toString() {order.push('name-string');return 'pay\ud800';}};}};
|
||||
const event = new C({toString() {order.push('type');return 'change\udc00';}}, init);
|
||||
const expectedOrder = ['type', 'bubbles', 'cancelable', 'composed'];
|
||||
if (name === 'PaymentMethodChangeEvent') expectedOrder.push('methodDetails', 'methodName', 'name-string');
|
||||
check(prefix + 'dictionary conversion order', order.join() === expectedOrder.join());
|
||||
check(prefix + 'UTF16 event type', event.type === 'change\udc00');
|
||||
check(prefix + 'event flags', event.bubbles && event.cancelable && event.composed && !event.isTrusted);
|
||||
if (name === 'PaymentMethodChangeEvent') {
|
||||
check(prefix + 'payload identity and UTF16', event.methodDetails === details && event.methodName === 'pay\ud800');
|
||||
for (const value of [undefined, null, {}, [], () => {}, new Proxy({}, {})]) {
|
||||
const result = new C('x', {methodDetails: value});
|
||||
check(prefix + 'nullable object ' + String(typeof value), result.methodDetails === (value == null ? null : value));
|
||||
}
|
||||
const revoked = Proxy.revocable({}, {}); revoked.revoke();
|
||||
check(prefix + 'revoked object payload retained', new C('x', {methodDetails: revoked.proxy}).methodDetails === revoked.proxy);
|
||||
for (const value of [true, 1, 'x', Symbol('s'), 2n]) throws(prefix + 'reject primitive payload ' + typeof value, () => new C('x', {methodDetails: value}), owner.TypeError);
|
||||
const token = {};
|
||||
let nameRead = false, caught;
|
||||
try {new C('x', {get methodDetails() {throw token;}, get methodName() {nameRead = true;return 'x';}});} catch(error) {caught = error;}
|
||||
check(prefix + 'dictionary exception and stop order', caught === token && !nameRead);
|
||||
}
|
||||
const target = new owner.EventTarget();
|
||||
const seen = [];
|
||||
target.addEventListener(event.type, received => {seen.push(received === event && received.target === target && received.currentTarget === target);});
|
||||
target.dispatchEvent(event);
|
||||
check(prefix + 'native EventTarget dispatch', seen.length === 1 && seen[0] && event.target === target && event.currentTarget === null && !event.isTrusted);
|
||||
for (let calleeIndex = 0; calleeIndex < realms.length; calleeIndex++) {
|
||||
const callee = realms[calleeIndex], q = callee.PaymentRequestUpdateEvent.prototype;
|
||||
const label = prefix + 'callee-' + calleeIndex + ':';
|
||||
const update = q.updateWith;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(q, 'updateWith');
|
||||
check(label + 'operation descriptor', descriptor.enumerable && descriptor.configurable && descriptor.writable && update.length === 1 && update.name === 'updateWith');
|
||||
const invalid = [null, undefined, {}, Object.create(p), Object.create(event), new Proxy(event, {})];
|
||||
const revoked = Proxy.revocable(event, {}); revoked.revoke(); invalid.push(revoked.proxy);
|
||||
for (let i = 0; i < invalid.length; i++) {
|
||||
let reads = 0;
|
||||
throws(label + 'reject receiver before conversion ' + i, () => update.call(invalid[i], {get then() {reads++;throw Error('conversion');}}), callee.TypeError);
|
||||
check(label + 'no receiver conversion ' + i, reads === 0);
|
||||
}
|
||||
throws(label + 'missing argument', () => update.call(event), callee.TypeError);
|
||||
for (const value of [undefined, null, {}, 1]) throws(label + 'untrusted value ' + typeof value, () => update.call(event, value), callee.DOMException, 'InvalidStateError');
|
||||
const calls = [];
|
||||
const value = {get then() {calls.push('get');return resolve => {calls.push('call');resolve({});};}};
|
||||
throws(label + 'thenable update error', () => update.call(event, value), callee.DOMException, 'InvalidStateError');
|
||||
check(label + 'then getter before state check', calls.join() === 'get');
|
||||
await Promise.resolve(); await Promise.resolve();
|
||||
check(label + 'then invoked asynchronously', calls.join() === 'get,call');
|
||||
const nativeCalls = [];
|
||||
const promise = owner.Promise.resolve({});
|
||||
Object.defineProperty(promise, 'then', {get() {nativeCalls.push('get');return resolve => {nativeCalls.push('call');resolve({});};}});
|
||||
Object.defineProperty(promise, 'constructor', {get() {throw Error('Promise constructor must not be read');}});
|
||||
throws(label + 'native promise update error', () => update.call(event, promise), callee.DOMException, 'InvalidStateError');
|
||||
check(label + 'WebIDL native Promise wrapping', nativeCalls.join() === 'get');
|
||||
await Promise.resolve(); await Promise.resolve();
|
||||
check(label + 'native Promise then invoked asynchronously', nativeCalls.join() === 'get,call');
|
||||
const delivered = new C('during');
|
||||
const dispatchOrder = [];
|
||||
target.addEventListener('during', () => {throws(label + 'untrusted during dispatch', () => update.call(delivered, {}), callee.DOMException, 'InvalidStateError');dispatchOrder.push(1);}, {once:true});
|
||||
target.addEventListener('during', () => dispatchOrder.push(2), {once:true});
|
||||
target.dispatchEvent(delivered);
|
||||
check(label + 'failed update leaves propagation intact', dispatchOrder.join() === '1,2');
|
||||
if (name === 'PaymentMethodChangeEvent') {
|
||||
for (const key of ['methodName', 'methodDetails']) {
|
||||
const desc = Object.getOwnPropertyDescriptor(callee.PaymentMethodChangeEvent.prototype, key);
|
||||
check(label + key + ':accessor descriptor', desc.enumerable && desc.configurable && desc.set === undefined);
|
||||
check(label + key + ':cross-realm getter', desc.get.call(event) === event[key]);
|
||||
for (let i = 0; i < invalid.length; i++) throws(label + key + ':brand ' + i, () => desc.get.call(invalid[i]), callee.TypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return checks;
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn payment_update_event_payloads_and_operations_use_native_cross_realm_brands() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://payment-events.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
|
||||
.unwrap();
|
||||
let source = format!(
|
||||
"({})([globalThis, document.getElementById('child').contentWindow]).then(checks => {{globalThis.__paymentChecks=checks;}})",
|
||||
include_str!("payment_events.js"),
|
||||
);
|
||||
vm.eval(&source).unwrap();
|
||||
assert_eq!(vm.eval("typeof __paymentChecks").unwrap(), "object");
|
||||
// Keep the native-Promise wrapping gaps explicit while the shared converter
|
||||
// still preserves native Promise inputs. Every other check must pass.
|
||||
let mut known_gaps = Vec::new();
|
||||
for owner in 0..2 {
|
||||
for event in ["PaymentRequestUpdateEvent", "PaymentMethodChangeEvent"] {
|
||||
for callee in 0..2 {
|
||||
for check in [
|
||||
"WebIDL native Promise wrapping",
|
||||
"native Promise then invoked asynchronously",
|
||||
] {
|
||||
known_gaps.push(format!("{owner}:{event}:callee-{callee}:{check}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
known_gaps.sort();
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
"JSON.stringify(__paymentChecks.filter(row => !row.passed).map(row => row.name).sort())"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::to_string(&known_gaps).unwrap()
|
||||
);
|
||||
assert_eq!(vm.eval("__paymentChecks.length").unwrap(), "378");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payment_event_payloads_and_update_with_accept_registered_native_proxies() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://payment-native-proxy.test/");
|
||||
vm.eval("globalThis.paymentEvent = new PaymentMethodChangeEvent('x', {methodName:'pay', methodDetails:{token:42}})")
|
||||
.unwrap();
|
||||
let context_ptr = &vm.page_default_runtime.context as *const _;
|
||||
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let key = crate::util::v8str(scope, "paymentEvent");
|
||||
let event =
|
||||
v8::Local::<v8::Object>::try_from(global.get(scope, key.into()).unwrap()).unwrap();
|
||||
let handler = crate::util::new_null_prototype_object(scope);
|
||||
let proxy = v8::Proxy::new(scope, event, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
|
||||
let key = crate::util::v8str(scope, "paymentProxy");
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), proxy.into()),
|
||||
Some(true)
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const p=PaymentMethodChangeEvent.prototype;
|
||||
for(const key of ['methodName','methodDetails']) {
|
||||
const get=Object.getOwnPropertyDescriptor(p,key).get;
|
||||
if(get.call(paymentProxy)!==paymentEvent[key]) throw Error('native identity');
|
||||
let caught=false;try{get.call(new Proxy(paymentProxy,{}));}catch(error){caught=error instanceof TypeError;}
|
||||
if(!caught) throw Error('author proxy accepted');
|
||||
}
|
||||
let gets=0, error;
|
||||
try{PaymentRequestUpdateEvent.prototype.updateWith.call(paymentProxy,{get then(){gets++;return undefined;}});}catch(caught){error=caught;}
|
||||
if(gets!==1 || !(error instanceof DOMException) || error.name!=='InvalidStateError') throw Error('native proxy operation');
|
||||
return true;
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payment_event_constructors_are_absent_in_insecure_window_realms() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("http://payment-events.test/");
|
||||
assert_eq!(vm.eval("[isSecureContext, 'PaymentRequestUpdateEvent' in globalThis, 'PaymentMethodChangeEvent' in globalThis].join(',')").unwrap(), "false,false,false");
|
||||
}
|
||||
@@ -193,6 +193,8 @@ interfaces! {
|
||||
GamepadButton;
|
||||
GamepadEvent: Event;
|
||||
PictureInPictureEvent: Event;
|
||||
PaymentRequestUpdateEvent: Event;
|
||||
PaymentMethodChangeEvent: PaymentRequestUpdateEvent;
|
||||
PictureInPictureWindow: EventTarget;
|
||||
GamepadHapticActuator;
|
||||
HTMLAllCollection;
|
||||
|
||||
@@ -1541,3 +1541,5 @@ mod tls;
|
||||
|
||||
mod canvas_fill_rect;
|
||||
mod xhr_failure;
|
||||
|
||||
mod payment_events;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn payment_update_event_interfaces_are_not_exposed_in_workers() {
|
||||
ensure_v8();
|
||||
let mut worker = spawn_worker(
|
||||
"postMessage([typeof PaymentRequestUpdateEvent, typeof PaymentMethodChangeEvent]);close();"
|
||||
.into(),
|
||||
"https://payment-worker.test/".into(),
|
||||
);
|
||||
let message = timeout(TIMEOUT, worker.recv()).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&expect_post_json(message)).unwrap(),
|
||||
serde_json::json!(["undefined", "undefined"])
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user