mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
fix(indexeddb): expose native version change event attributes
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
globalThis.versionChangeChecks = [];
|
||||
async function versionChangeProbe(prefix = 'version-change') {
|
||||
const checks = versionChangeChecks;
|
||||
const check = (label, pass, actual = '') => checks.push({label, pass, actual: String(actual)});
|
||||
const same = (label, actual, expected) => check(label, JSON.stringify(actual) === JSON.stringify(expected), JSON.stringify(actual));
|
||||
const throws = (label, action, Realm) => {
|
||||
let caught;
|
||||
try { action(); } catch (error) { caught = error; }
|
||||
check(label, caught !== undefined && Object.getPrototypeOf(caught) === Realm.TypeError.prototype, caught);
|
||||
};
|
||||
const exercise = async (label, Realm, GetterRealm) => {
|
||||
const Ctor = Realm.IDBVersionChangeEvent;
|
||||
const getters = {};
|
||||
for (const name of ['oldVersion', 'newVersion']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(GetterRealm.IDBVersionChangeEvent.prototype, name);
|
||||
getters[name] = descriptor?.get;
|
||||
check(label + ': ' + name + ' descriptor', typeof descriptor?.get === 'function' &&
|
||||
descriptor.set === undefined && descriptor.enumerable && descriptor.configurable);
|
||||
check(label + ': ' + name + ' getter metadata', descriptor?.get?.name === 'get ' + name && descriptor.get.length === 0);
|
||||
}
|
||||
const get = (name, receiver) => typeof getters[name] === 'function' ? getters[name].call(receiver) : undefined;
|
||||
check(label + ': constructor length', Ctor.length === 1, Ctor.length);
|
||||
throws(label + ': new is required', () => Ctor('type'), Realm);
|
||||
throws(label + ': type is required', () => new Ctor(), Realm);
|
||||
throws(label + ': symbol type is rejected', () => new Ctor(Symbol()), Realm);
|
||||
for (const invalid of [1, 'init', Symbol(), 1n]) {
|
||||
throws(label + ': primitive dictionary is rejected', () => new Ctor('type', invalid), Realm);
|
||||
}
|
||||
for (const init of [undefined, null, {}]) {
|
||||
const event = new Ctor('default', init);
|
||||
same(label + ': default versions', [event.oldVersion, event.newVersion], [0, null]);
|
||||
same(label + ': default flags', [event.bubbles, event.cancelable, event.composed, event.isTrusted], [false, false, false, false]);
|
||||
}
|
||||
const order = [];
|
||||
const init = {};
|
||||
for (const [name, value] of [['bubbles', true], ['cancelable', true], ['composed', true],
|
||||
['newVersion', {valueOf() { order.push('convert newVersion'); return 9; }}],
|
||||
['oldVersion', {valueOf() { order.push('convert oldVersion'); return 7; }}]]) {
|
||||
Object.defineProperty(init, name, {get() { order.push(name); return value; }});
|
||||
}
|
||||
const event = new Ctor({toString() { order.push('type'); return 'flags'; }}, Object.create(init));
|
||||
same(label + ': conversion order', order,
|
||||
['type', 'bubbles', 'cancelable', 'composed', 'newVersion', 'convert newVersion', 'oldVersion', 'convert oldVersion']);
|
||||
for (const name of ['bubbles', 'cancelable', 'composed']) {
|
||||
check(label + ': EventInit ' + name, event[name] === true, event[name]);
|
||||
}
|
||||
const target = new EventTarget();
|
||||
target.addEventListener('flags', value => value.preventDefault());
|
||||
check(label + ': EventInit dispatch cancellation', target.dispatchEvent(event) === false);
|
||||
check(label + ': EventInit defaultPrevented', event.defaultPrevented === true);
|
||||
for (const [name, expected] of [['oldVersion', 7], ['newVersion', 9]]) {
|
||||
check(label + ': ' + name + ' inherited', !Object.hasOwn(event, name));
|
||||
check(label + ': ' + name + ' getter value', get(name, event) === expected, get(name, event));
|
||||
check(label + ': ' + name + ' readonly', Reflect.set(event, name, 99) === false);
|
||||
check(label + ': ' + name + ' unchanged', event[name] === expected, event[name]);
|
||||
Object.defineProperty(event, name, {configurable: true, get() { throw new Error('author shadow'); }});
|
||||
check(label + ': ' + name + ' native value under author shadow', get(name, event) === expected, get(name, event));
|
||||
delete event[name];
|
||||
check(label + ': ' + name + ' restored getter', event[name] === expected, event[name]);
|
||||
}
|
||||
const revoked = Proxy.revocable(event, {}); revoked.revoke();
|
||||
let traps = 0;
|
||||
const trapped = new Proxy(event, {get() { ++traps; throw new Error('trap'); }, getPrototypeOf() { ++traps; throw new Error('trap'); }});
|
||||
for (const receiver of [null, undefined, 1, {}, Ctor.prototype, Object.create(Ctor.prototype),
|
||||
Object.create(event), new Realm.Event('base'), new Proxy(event, {}), revoked.proxy, trapped]) {
|
||||
for (const name of ['oldVersion', 'newVersion']) {
|
||||
throws(label + ': ' + name + ' invalid receiver', () => get(name, receiver), GetterRealm);
|
||||
}
|
||||
}
|
||||
check(label + ': no Proxy traps', traps === 0, traps);
|
||||
Object.setPrototypeOf(event, null);
|
||||
same(label + ': native identity survives prototype removal', [get('oldVersion', event), get('newVersion', event)], [7, 9]);
|
||||
class Subclass extends Ctor {}
|
||||
const sub = new Subclass('sub', {oldVersion: 3, newVersion: 4});
|
||||
check(label + ': subclass prototype is preserved', Object.getPrototypeOf(sub) === Subclass.prototype);
|
||||
same(label + ': subclass native identity', [get('oldVersion', sub), get('newVersion', sub)], [3, 4]);
|
||||
|
||||
const members = ['bubbles', 'cancelable', 'composed', 'newVersion', 'oldVersion'];
|
||||
for (let stop = 0; stop < members.length; ++stop) {
|
||||
const reads = [];
|
||||
const marker = new Error('dictionary getter');
|
||||
const options = new Proxy({}, {get(_target, key) {
|
||||
reads.push(key);
|
||||
if (key === members[stop]) throw marker;
|
||||
return undefined;
|
||||
}});
|
||||
let caught;
|
||||
try { new Ctor('throws', options); } catch (error) { caught = error; }
|
||||
check(label + ': getter exception identity ' + members[stop], caught === marker, caught);
|
||||
same(label + ': stop conversion at ' + members[stop], reads, members.slice(0, stop + 1));
|
||||
}
|
||||
const conversions = [[undefined, 0, null], [null, 0, null], [NaN, 0, 0], [Infinity, 0, 0],
|
||||
[-Infinity, 0, 0], [1.9, 1, 1], [-1, 2 ** 64, 2 ** 64], [2 ** 64, 0, 0], ['12', 12, 12]];
|
||||
for (const [value, oldVersion, newVersion] of conversions) {
|
||||
const converted = new Ctor('number', {oldVersion: value, newVersion: value});
|
||||
same(label + ': numeric conversion ' + String(value), [converted.oldVersion, converted.newVersion], [oldVersion, newVersion]);
|
||||
}
|
||||
for (const name of ['oldVersion', 'newVersion']) {
|
||||
for (const value of [Symbol(), 1n]) {
|
||||
throws(label + ': reject invalid ' + name, () => new Ctor('invalid', {[name]: value}), Realm);
|
||||
}
|
||||
}
|
||||
const marker = new Error('type conversion');
|
||||
let dictionaryReads = 0;
|
||||
let caught;
|
||||
try {
|
||||
new Ctor({toString() { throw marker; }}, new Proxy({}, {get() { ++dictionaryReads; }}));
|
||||
} catch (error) { caught = error; }
|
||||
check(label + ': type exception identity', caught === marker);
|
||||
check(label + ': type exception stops dictionary conversion', dictionaryReads === 0, dictionaryReads);
|
||||
|
||||
const nativeEvents = [];
|
||||
const seen = [];
|
||||
let inspectionError;
|
||||
const inspectNative = (name, value, oldVersion, newVersion) => {
|
||||
try {
|
||||
seen.push(name);
|
||||
same(label + ': native ' + name + ' versions', [value.oldVersion, value.newVersion], [oldVersion, newVersion]);
|
||||
same(label + ': native ' + name + ' flags', [value.bubbles, value.cancelable, value.composed], [false, false, false]);
|
||||
check(label + ': native ' + name + ' prototype', Object.getPrototypeOf(value) === Ctor.prototype);
|
||||
check(label + ': native ' + name + ' inherited attributes', !Object.hasOwn(value, 'oldVersion') && !Object.hasOwn(value, 'newVersion'));
|
||||
same(label + ': native ' + name + ' borrowed getters', [get('oldVersion', value), get('newVersion', value)], [oldVersion, newVersion]);
|
||||
check(label + ': native ' + name + ' readonly oldVersion', Reflect.set(value, 'oldVersion', 99) === false);
|
||||
check(label + ': native ' + name + ' readonly newVersion', Reflect.set(value, 'newVersion', 99) === false);
|
||||
nativeEvents.push([name, value, oldVersion, newVersion]);
|
||||
} catch (error) { inspectionError ??= error; }
|
||||
};
|
||||
const requestResult = request => new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
const factory = Realm.indexedDB;
|
||||
const name = prefix + '-' + label;
|
||||
const savedCtor = Realm.IDBVersionChangeEvent;
|
||||
Realm.IDBVersionChangeEvent = function() { throw new Error('author constructor'); };
|
||||
try {
|
||||
const first = factory.open(name, 1);
|
||||
first.onupgradeneeded = value => inspectNative('create', value, 0, 1);
|
||||
const oldDb = await requestResult(first);
|
||||
oldDb.onversionchange = value => inspectNative('versionchange', value, 1, 2);
|
||||
const second = factory.open(name, 2);
|
||||
second.onblocked = value => { inspectNative('blocked', value, 1, 2); oldDb.close(); };
|
||||
second.onupgradeneeded = value => inspectNative('upgrade', value, 1, 2);
|
||||
const newDb = await requestResult(second);
|
||||
newDb.onversionchange = value => { inspectNative('delete notification', value, 2, null); newDb.close(); };
|
||||
await requestResult(factory.deleteDatabase(name));
|
||||
} finally { Realm.IDBVersionChangeEvent = savedCtor; }
|
||||
if (inspectionError) throw inspectionError;
|
||||
same(label + ': native event order', seen,
|
||||
['create', 'versionchange', 'blocked', 'upgrade', 'delete notification']);
|
||||
for (const [name, value, oldVersion, newVersion] of nativeEvents) {
|
||||
same(label + ': retained native ' + name, [get('oldVersion', value), get('newVersion', value)], [oldVersion, newVersion]);
|
||||
}
|
||||
};
|
||||
await exercise('local', globalThis, globalThis);
|
||||
if (typeof document !== 'undefined') {
|
||||
const frame = document.createElement('iframe');
|
||||
const loaded = new Promise(resolve => { frame.onload = resolve; });
|
||||
frame.srcdoc = '<!doctype html>'; document.documentElement.appendChild(frame); await loaded;
|
||||
try {
|
||||
await exercise('foreign-getter', globalThis, frame.contentWindow);
|
||||
await exercise('foreign-event', frame.contentWindow, globalThis);
|
||||
} finally { frame.remove(); }
|
||||
}
|
||||
return {state: checks.every(check => check.pass) ? 'pass' : 'fail', checks};
|
||||
}
|
||||
@@ -1,6 +1,28 @@
|
||||
use super::pipe_disturbed::run_probe;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn indexed_db_version_change_events_preserve_native_attributes_and_dictionary_semantics()
|
||||
-> Result<()> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
let fixture = include_str!("fixtures/indexeddb-version-change.js");
|
||||
for target in ["window", "child", "worker"] {
|
||||
let source = format!(
|
||||
"{fixture}\nversionChangeProbe('version-change-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: versionChangeChecks}}));"
|
||||
);
|
||||
let result = run_probe(&browser, &server, target, &source).await?;
|
||||
assert_eq!(result["state"], "pass", "{target}: {result}");
|
||||
assert_eq!(
|
||||
result["checks"].as_array().unwrap().len(),
|
||||
if target == "worker" { 128 } else { 384 },
|
||||
"{target}: {result}"
|
||||
);
|
||||
}
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn indexed_db_factory_checks_native_receivers_and_rejects_promises_in_callee_realm()
|
||||
-> Result<()> {
|
||||
|
||||
@@ -938,7 +938,7 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
|
||||
web_api_interfaces::IDBVersionChangeEvent,
|
||||
crate::context_bootstrap::indexed_db::idb_version_change_event_constructor_callback
|
||||
))
|
||||
.length(2)
|
||||
.length(1)
|
||||
.build(scope)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,62 @@
|
||||
use super::super::super::events;
|
||||
use super::*;
|
||||
use crate::web_api_interfaces;
|
||||
use crate::webidl;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
const OLD_VERSION_SLOT: &str = "__moli_idb_event_old_version";
|
||||
const NEW_VERSION_SLOT: &str = "__moli_idb_event_new_version";
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
#[webapi(fragment)]
|
||||
struct IdbVersionChangeEventFieldsDeclaration<'scope> {
|
||||
#[webapi(slot = OLD_VERSION_SLOT)]
|
||||
old_version: u64,
|
||||
#[webapi(slot = NEW_VERSION_SLOT)]
|
||||
new_version: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::IDBVersionChangeEvent, enumerable, receiver)]
|
||||
struct IdbVersionChangeEventPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = old_version_getter)]
|
||||
old_version: (),
|
||||
#[webapi(accessor_property, getter = new_version_getter)]
|
||||
new_version: (),
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn install_version_change_event_template_bindings<
|
||||
's,
|
||||
>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
prototype: v8::Local<'s, v8::ObjectTemplate>,
|
||||
name: &str,
|
||||
) {
|
||||
if name == "IDBVersionChangeEvent" {
|
||||
IdbVersionChangeEventPrototypeDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
}
|
||||
|
||||
fn old_version_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if let Some(value) = events::event_private_value(scope, args.this(), OLD_VERSION_SLOT) {
|
||||
rv.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_version_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if let Some(value) = events::event_private_value(scope, args.this(), NEW_VERSION_SLOT) {
|
||||
rv.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "IDBVersionChangeEvent")]
|
||||
struct IdbVersionChangeEventConstructorArgs {
|
||||
@@ -22,10 +69,18 @@ struct IdbVersionChangeEventConstructorArgs {
|
||||
#[derive(Default, webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "IDBVersionChangeEventInit")]
|
||||
struct IdbVersionChangeEventInit {
|
||||
#[webidl(default = 0)]
|
||||
old_version: u64,
|
||||
// WebIDL reads inherited EventInit members first, then this dictionary's
|
||||
// members in lexicographic order. The derive preserves declaration order.
|
||||
#[webidl(default = false)]
|
||||
bubbles: bool,
|
||||
#[webidl(default = false)]
|
||||
cancelable: bool,
|
||||
#[webidl(default = false)]
|
||||
composed: bool,
|
||||
#[webidl(nullable)]
|
||||
new_version: Option<u64>,
|
||||
#[webidl(default = 0)]
|
||||
old_version: u64,
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn idb_version_change_event_constructor_callback<'s>(
|
||||
@@ -46,7 +101,19 @@ pub(in crate::context_bootstrap) fn idb_version_change_event_constructor_callbac
|
||||
};
|
||||
let wrapper = args.this();
|
||||
let event = events::new_event_state(scope);
|
||||
events::initialize_event_object(scope, event, &parsed.event_type, false, false);
|
||||
events::initialize_event_object(
|
||||
scope,
|
||||
event,
|
||||
&parsed.event_type,
|
||||
parsed.init.bubbles,
|
||||
parsed.init.cancelable,
|
||||
);
|
||||
events::define_event_property(
|
||||
scope,
|
||||
event,
|
||||
"composed",
|
||||
v8::Boolean::new(scope, parsed.init.composed).into(),
|
||||
);
|
||||
IdbVersionChangeEventFieldsDeclaration::new(
|
||||
parsed.init.old_version,
|
||||
version_change_nullable_version_value(scope, parsed.init.new_version),
|
||||
|
||||
@@ -11,6 +11,11 @@ pub(in crate::context_bootstrap) fn install_indexed_db_template_bindings<'s>(
|
||||
interface_name: &str,
|
||||
) {
|
||||
let prototype = template.prototype_template(scope);
|
||||
super::super::event_target::install_version_change_event_template_bindings(
|
||||
scope,
|
||||
prototype,
|
||||
interface_name,
|
||||
);
|
||||
install_dom_string_list_template_bindings(scope, prototype, interface_name);
|
||||
factory_request::install_factory_and_request_template_bindings(
|
||||
scope,
|
||||
|
||||
Reference in New Issue
Block a user