diff options
| -rw-r--r-- | packages/bun-types/globals.d.ts | 24 | ||||
| -rw-r--r-- | src/bun.js/api/bun.zig | 2 | ||||
| -rw-r--r-- | src/bun.js/bindings/JSDOMFile.cpp | 105 | ||||
| -rw-r--r-- | src/bun.js/bindings/JSDOMFile.h | 7 | ||||
| -rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 32 | ||||
| -rw-r--r-- | src/bun.js/bindings/blob.cpp | 14 | ||||
| -rw-r--r-- | src/bun.js/modules/NodeBufferModule.h | 7 | ||||
| -rw-r--r-- | src/bun.js/webcore/blob.zig | 126 | ||||
| -rw-r--r-- | test/js/bun/globals.test.js | 75 | ||||
| -rw-r--r-- | test/js/node/buffer.test.js | 2 | ||||
| -rw-r--r-- | test/js/web/html/FormData.test.ts | 4 | ||||
| -rw-r--r-- | test/js/web/web-globals.test.js | 1 | 
12 files changed, 387 insertions, 12 deletions
| diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 1a10cc8e0..740cc9690 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -827,7 +827,7 @@ type ResponseType =    | "opaque"    | "opaqueredirect"; -type FormDataEntryValue = Blob | string; +type FormDataEntryValue = File | string;  /** Provides a way to easily construct a set of key/value pairs representing   * form fields and their values, which can then be easily sent using the @@ -959,6 +959,28 @@ declare var Blob: {    new (parts?: BlobPart[], options?: BlobPropertyBag): Blob;  }; +interface File extends Blob { +  readonly lastModified: number; +  readonly name: string; +} + +declare var File: { +  prototype: File; + +  /** +   * Create a new [File](https://developer.mozilla.org/en-US/docs/Web/API/File) +   * +   * @param `parts` - An array of strings, numbers, BufferSource, or [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects +   * @param `name` - The name of the file +   * @param `options` - An object containing properties to be added to the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) +   */ +  new ( +    parts: BlobPart[], +    name: string, +    options?: BlobPropertyBag & { lastModified?: Date | number }, +  ): File; +}; +  interface ResponseInit {    headers?: HeadersInit;    /** @default 200 */ diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index e98151fb1..8d1244195 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -15,7 +15,7 @@ pub const BunObject = struct {      pub const build = Bun.JSBundler.buildFn;      pub const connect = JSC.wrapStaticMethod(JSC.API.Listener, "connect", false);      pub const deflateSync = JSC.wrapStaticMethod(JSZlib, "deflateSync", true); -    pub const file = WebCore.Blob.constructFile; +    pub const file = WebCore.Blob.constructBunFile;      pub const fs = Bun.fs;      pub const gc = Bun.runGC;      pub const generateHeapSnapshot = Bun.generateHeapSnapshot; diff --git a/src/bun.js/bindings/JSDOMFile.cpp b/src/bun.js/bindings/JSDOMFile.cpp new file mode 100644 index 000000000..1d7770ac1 --- /dev/null +++ b/src/bun.js/bindings/JSDOMFile.cpp @@ -0,0 +1,105 @@ +#include "root.h" +#include "ZigGeneratedClasses.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/InternalFunction.h" +#include "JavaScriptCore/FunctionPrototype.h" +#include "JSDOMFile.h" + +using namespace JSC; + +extern "C" void* JSDOMFile__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); +extern "C" bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); + +// TODO: make this inehrit from JSBlob instead of InternalFunction +// That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain. +class JSDOMFile : public JSC::InternalFunction { +    using Base = JSC::InternalFunction; + +public: +    JSDOMFile(JSC::VM& vm, JSC::Structure* structure) +        : Base(vm, structure, nullptr, construct) +    { +    } + +    DECLARE_INFO; + +    static constexpr unsigned StructureFlags = (Base::StructureFlags & ~ImplementsDefaultHasInstance) | ImplementsHasInstance; + +    template<typename CellType, JSC::SubspaceAccess> +    static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) +    { +        return &vm.internalFunctionSpace(); +    } +    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +    { +        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(InternalFunctionType, StructureFlags), info()); +    } + +    void finishCreation(JSC::VM& vm) +    { +        Base::finishCreation(vm, 2, "File"_s); +    } + +    static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject) +    { +        auto* zigGlobal = reinterpret_cast<Zig::GlobalObject*>(globalObject); +        auto* object = new (NotNull, JSC::allocateCell<JSDOMFile>(vm)) JSDOMFile(vm, createStructure(vm, globalObject, zigGlobal->functionPrototype())); +        object->finishCreation(vm); + +        // This is not quite right. But we'll fix it if someone files an issue about it. +        object->putDirect(vm, vm.propertyNames->prototype, zigGlobal->JSBlobPrototype(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); + +        return object; +    } + +    static bool customHasInstance(JSObject* object, JSGlobalObject* globalObject, JSValue value) +    { +        if (!value.isObject()) +            return false; + +        // Note: this breaks [Symbol.hasInstance] +        // We must do this for now until we update the code generator to export classes +        return JSDOMFile__hasInstance(JSValue::encode(object), globalObject, JSValue::encode(value)); +    } + +    static EncodedJSValue construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +    { +        Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); +        JSC::VM& vm = globalObject->vm(); +        JSObject* newTarget = asObject(callFrame->newTarget()); +        auto* constructor = globalObject->JSDOMFileConstructor(); +        Structure* structure = globalObject->JSBlobStructure(); +        if (constructor != newTarget) { +            auto scope = DECLARE_THROW_SCOPE(vm); + +            auto* functionGlobalObject = reinterpret_cast<Zig::GlobalObject*>( +                // ShadowRealm functions belong to a different global object. +                getFunctionRealm(globalObject, newTarget)); +            RETURN_IF_EXCEPTION(scope, {}); +            structure = InternalFunction::createSubclassStructure( +                globalObject, +                newTarget, +                functionGlobalObject->JSBlobStructure()); +        } + +        void* ptr = JSDOMFile__construct(globalObject, callFrame); + +        if (UNLIKELY(!ptr)) { +            return JSValue::encode(JSC::jsUndefined()); +        } + +        return JSValue::encode( +            WebCore::JSBlob::create(vm, globalObject, structure, ptr)); +    } +}; + +const JSC::ClassInfo JSDOMFile::s_info = { "File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMFile) }; + +namespace Bun { + +JSC::JSObject* createJSDOMFileConstructor(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ +    return JSDOMFile::create(vm, globalObject); +} + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/JSDOMFile.h b/src/bun.js/bindings/JSDOMFile.h new file mode 100644 index 000000000..eed1e98ab --- /dev/null +++ b/src/bun.js/bindings/JSDOMFile.h @@ -0,0 +1,7 @@ +#pragma once + +#include "root.h" + +namespace Bun { +JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*); +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index f8fc2ea2b..0ecafeae4 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -123,6 +123,8 @@  #include "JSMessagePort.h"  #include "JSBroadcastChannel.h" +#include "JSDOMFile.h" +  #if ENABLE(REMOTE_INSPECTOR)  #include "JavaScriptCore/RemoteInspectorServer.h"  #endif @@ -2780,6 +2782,12 @@ void GlobalObject::finishCreation(VM& vm)      Base::finishCreation(vm);      ASSERT(inherits(info())); +    m_JSDOMFileConstructor.initLater( +        [](const Initializer<JSObject>& init) { +            JSObject* fileConstructor = Bun::createJSDOMFileConstructor(init.vm, init.owner); +            init.set(fileConstructor); +        }); +      m_cryptoObject.initLater(          [](const Initializer<JSObject>& init) {              JSC::JSGlobalObject* globalObject = init.owner; @@ -3330,6 +3338,26 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPostMessage,      return JSValue::encode(jsUndefined());  } +JSC_DEFINE_CUSTOM_GETTER(JSDOMFileConstructor_getter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ +    Zig::GlobalObject* bunGlobalObject = jsCast<Zig::GlobalObject*>(globalObject); +    return JSValue::encode( +        bunGlobalObject->JSDOMFileConstructor()); +} + +JSC_DEFINE_CUSTOM_SETTER(JSDOMFileConstructor_setter, +    (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, +        JSC::EncodedJSValue value, JSC::PropertyName property)) +{ +    if (JSValue::decode(thisValue) != globalObject) { +        return false; +    } + +    auto& vm = globalObject->vm(); +    globalObject->putDirect(vm, property, JSValue::decode(value), 0); +    return true; +} +  JSC_DEFINE_CUSTOM_GETTER(BunCommonJSModule_getter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))  {      Zig::GlobalObject* bunGlobalObject = jsCast<Zig::GlobalObject*>(globalObject); @@ -3777,6 +3805,9 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)      putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "Crypto"_s), JSC::CustomGetterSetter::create(vm, JSCrypto_getter, JSCrypto_setter),          JSC::PropertyAttribute::DontDelete | 0); +    putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "File"_s), JSC::CustomGetterSetter::create(vm, JSDOMFileConstructor_getter, JSDOMFileConstructor_setter), +        JSC::PropertyAttribute::DontDelete | 0); +      putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "DOMException"_s), JSC::CustomGetterSetter::create(vm, JSDOMException_getter, nullptr),          JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); @@ -3996,6 +4027,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)      thisObject->m_emitReadableNextTickFunction.visit(visitor);      thisObject->m_JSBufferSubclassStructure.visit(visitor);      thisObject->m_cryptoObject.visit(visitor); +    thisObject->m_JSDOMFileConstructor.visit(visitor);      thisObject->m_requireFunctionUnbound.visit(visitor);      thisObject->m_requireResolveFunctionUnbound.visit(visitor); diff --git a/src/bun.js/bindings/blob.cpp b/src/bun.js/bindings/blob.cpp index 257f230e1..0f255d7c8 100644 --- a/src/bun.js/bindings/blob.cpp +++ b/src/bun.js/bindings/blob.cpp @@ -1,17 +1,29 @@  #include "blob.h" +#include "ZigGeneratedClasses.h"  extern "C" JSC::EncodedJSValue Blob__create(JSC::JSGlobalObject* globalObject, void* impl); +extern "C" void* Blob__setAsFile(void* impl, BunString* filename);  namespace WebCore {  JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WebCore::Blob& impl)  { +    BunString filename = Bun::toString(impl.fileName()); +    impl.m_impl = Blob__setAsFile(impl.impl(), &filename); +      return JSC::JSValue::decode(Blob__create(lexicalGlobalObject, Blob__dupe(impl.impl())));  }  JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Ref<WebCore::Blob>&& impl)  { -    return JSC::JSValue::decode(Blob__create(lexicalGlobalObject, impl->impl())); +    auto fileNameStr = impl->fileName(); +    BunString filename = Bun::toString(fileNameStr); + +    EncodedJSValue encoded = Blob__create(lexicalGlobalObject, impl->impl()); +    JSBlob* blob = jsCast<JSBlob*>(JSC::JSValue::decode(encoded)); +    Blob__setAsFile(blob->wrapped(), &filename); + +    return JSC::JSValue::decode(encoded);  }  }
\ No newline at end of file diff --git a/src/bun.js/modules/NodeBufferModule.h b/src/bun.js/modules/NodeBufferModule.h index 5c6acd48e..5eea9c099 100644 --- a/src/bun.js/modules/NodeBufferModule.h +++ b/src/bun.js/modules/NodeBufferModule.h @@ -149,12 +149,11 @@ DEFINE_NATIVE_MODULE(NodeBuffer) {    put(JSC::Identifier::fromString(vm, "SlowBuffer"_s), slowBuffer);    auto blobIdent = JSC::Identifier::fromString(vm, "Blob"_s); -  JSValue blobValue = -      lexicalGlobalObject->get(globalObject, PropertyName(blobIdent)); +  JSValue blobValue = globalObject->JSBlobConstructor();    put(blobIdent, blobValue); -  // TODO: implement File -  put(JSC::Identifier::fromString(vm, "File"_s), blobValue); +  put(JSC::Identifier::fromString(vm, "File"_s), +      globalObject->JSDOMFileConstructor());    put(JSC::Identifier::fromString(vm, "INSPECT_MAX_BYTES"_s),        JSC::jsNumber(50)); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 983841581..df2e17ce4 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -88,8 +88,13 @@ pub const Blob = struct {      /// When UTF-16, they're nearly always due to non-ascii characters      is_all_ascii: ?bool = null, +    /// Was it created via file constructor? +    is_jsdom_file: bool = false, +      globalThis: *JSGlobalObject = undefined, +    last_modified: f64 = 0.0, +      /// Max int of double precision      /// 9 petabytes is probably enough for awhile      /// We want to avoid coercing to a BigInt because that's a heap allocation @@ -478,6 +483,22 @@ pub const Blob = struct {          return Blob__dupe(this);      } +    export fn Blob__setAsFile(this: *Blob, path_str: *bun.String) *Blob { +        this.is_jsdom_file = true; + +        // This is not 100% correct... +        if (this.store) |store| { +            if (store.data == .bytes) { +                if (store.data.bytes.stored_name.len == 0) { +                    var utf8 = path_str.toUTF8WithoutRef(bun.default_allocator).clone(bun.default_allocator) catch unreachable; +                    store.data.bytes.stored_name = bun.PathString.init(utf8.slice()); +                } +            } +        } + +        return this; +    } +      export fn Blob__dupe(ptr: *anyopaque) *Blob {          var this = bun.cast(*Blob, ptr);          var new = bun.default_allocator.create(Blob) catch unreachable; @@ -494,6 +515,7 @@ pub const Blob = struct {          _ = Blob__dupeFromJS;          _ = Blob__destroy;          _ = Blob__dupe; +        _ = Blob__setAsFile;      }      pub fn writeFormatForSize(size: usize, writer: anytype, comptime enable_ansi_colors: bool) !void { @@ -1123,7 +1145,102 @@ pub const Blob = struct {          return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written));      } -    pub fn constructFile( +    pub export fn JSDOMFile__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(.C) bool { +        JSC.markBinding(@src()); +        var blob = value.as(Blob) orelse return false; +        return blob.is_jsdom_file; +    } + +    pub export fn JSDOMFile__construct( +        globalThis: *JSC.JSGlobalObject, +        callframe: *JSC.CallFrame, +    ) callconv(.C) ?*Blob { +        JSC.markBinding(@src()); +        var allocator = bun.default_allocator; +        var blob: Blob = undefined; +        var arguments = callframe.arguments(3); +        var args = arguments.ptr[0..arguments.len]; + +        if (args.len < 2) { +            globalThis.throwInvalidArguments("new File(bits, name) expects at least 2 arguments", .{}); +            return null; +        } + +        const name_value_str = bun.String.tryFromJS(args[1], globalThis) orelse { +            globalThis.throwInvalidArguments("new File(bits, name) expects string as the second argument", .{}); +            return null; +        }; + +        blob = get(globalThis, args[0], false, true) catch |err| { +            if (err == error.InvalidArguments) { +                globalThis.throwInvalidArguments("new File(bits, name) expects iterable as the first argument", .{}); +                return null; +            } +            globalThis.throwOutOfMemory(); +            return null; +        }; + +        if (blob.store) |store_| { +            store_.data.bytes.stored_name = bun.PathString.init( +                (name_value_str.toUTF8WithoutRef(bun.default_allocator).clone(bun.default_allocator) catch unreachable).slice(), +            ); +        } + +        if (args.len > 2) { +            const options = args[2]; +            if (options.isObject()) { +                // type, the ASCII-encoded string in lower case +                // representing the media type of the Blob. +                // Normative conditions for this member are provided +                // in the § 3.1 Constructors. +                if (options.get(globalThis, "type")) |content_type| { +                    inner: { +                        if (content_type.isString()) { +                            var content_type_str = content_type.toSlice(globalThis, bun.default_allocator); +                            defer content_type_str.deinit(); +                            var slice = content_type_str.slice(); +                            if (!strings.isAllASCII(slice)) { +                                break :inner; +                            } +                            blob.content_type_was_set = true; + +                            if (globalThis.bunVM().mimeType(slice)) |mime| { +                                blob.content_type = mime.value; +                                break :inner; +                            } +                            var content_type_buf = allocator.alloc(u8, slice.len) catch unreachable; +                            blob.content_type = strings.copyLowercase(slice, content_type_buf); +                            blob.content_type_allocated = true; +                        } +                    } +                } + +                if (options.getTruthy(globalThis, "lastModified")) |last_modified| { +                    blob.last_modified = last_modified.coerce(f64, globalThis); +                } +            } +        } + +        if (blob.content_type.len == 0) { +            blob.content_type = ""; +            blob.content_type_was_set = false; +        } + +        var blob_ = allocator.create(Blob) catch unreachable; +        blob_.* = blob; +        blob_.allocator = allocator; +        blob_.is_jsdom_file = true; +        return blob_; +    } + +    comptime { +        if (!JSC.is_bindgen) { +            _ = JSDOMFile__hasInstance; +            _ = JSDOMFile__construct; +        } +    } + +    pub fn constructBunFile(          globalObject: *JSC.JSGlobalObject,          callframe: *JSC.CallFrame,      ) callconv(.C) JSC.JSValue { @@ -2484,7 +2601,7 @@ pub const Blob = struct {          cap: SizeType = 0,          allocator: std.mem.Allocator, -        /// Used by standalone module graph +        /// Used by standalone module graph and the File constructor          stored_name: bun.PathString = bun.PathString.empty,          pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { @@ -2505,6 +2622,7 @@ pub const Blob = struct {          }          pub fn deinit(this: *ByteStore) void { +            bun.default_allocator.free(this.stored_name.slice());              this.allocator.free(this.ptr[0..this.cap]);          } @@ -2911,6 +3029,10 @@ pub const Blob = struct {              }          } +        if (this.is_jsdom_file) { +            return JSValue.jsNumber(this.last_modified); +        } +          return JSValue.jsNumber(init_timestamp);      } diff --git a/test/js/bun/globals.test.js b/test/js/bun/globals.test.js index 6b004a5f7..fd291d8bc 100644 --- a/test/js/bun/globals.test.js +++ b/test/js/bun/globals.test.js @@ -1,4 +1,4 @@ -import { expect, it } from "bun:test"; +import { expect, it, describe } from "bun:test";  it("extendable", () => {    const classes = [Blob, TextDecoder, TextEncoder, Request, Response, Headers, HTMLRewriter, Bun.Transpiler, Buffer]; @@ -25,6 +25,7 @@ it("writable", () => {      ["ErrorEvent", ErrorEvent],      ["CustomEvent", CustomEvent],      ["CloseEvent", CloseEvent], +    ["File", File],    ];    for (let [name, Class] of classes) {      globalThis[name] = 123; @@ -45,8 +46,80 @@ it("name", () => {      ["HTMLRewriter", HTMLRewriter],      ["Transpiler", Bun.Transpiler],      ["Buffer", Buffer], +    ["File", File],    ];    for (let [name, Class] of classes) {      expect(Class.name).toBe(name);    }  }); + +describe("File", () => { +  it("constructor", () => { +    const file = new File(["foo"], "bar.txt", { type: "text/plain;charset=utf-8" }); +    expect(file.name).toBe("bar.txt"); +    expect(file.type).toBe("text/plain;charset=utf-8"); +    expect(file.size).toBe(3); +    expect(file.lastModified).toBe(0); +  }); + +  it("constructor with lastModified", () => { +    const file = new File(["foo"], "bar.txt", { type: "text/plain;charset=utf-8", lastModified: 123 }); +    expect(file.name).toBe("bar.txt"); +    expect(file.type).toBe("text/plain;charset=utf-8"); +    expect(file.size).toBe(3); +    expect(file.lastModified).toBe(123); +  }); + +  it("constructor with undefined name", () => { +    const file = new File(["foo"], undefined); +    expect(file.name).toBe("undefined"); +    expect(file.type).toBe(""); +    expect(file.size).toBe(3); +    expect(file.lastModified).toBe(0); +  }); + +  it("constructor throws invalid args", () => { +    const invalid = [[], [undefined], [null], [Symbol(), "foo"], [Symbol(), Symbol(), Symbol()]]; +    for (let args of invalid) { +      expect(() => new File(...args)).toThrow(); +    } +  }); + +  it("instanceof", () => { +    const file = new File(["foo"], "bar.txt", { type: "text/plain;charset=utf-8" }); +    expect(file instanceof File).toBe(true); +    expect(file instanceof Blob).toBe(true); +    expect(file instanceof Object).toBe(true); +    expect(file instanceof Function).toBe(false); +    const blob = new Blob(["foo"], { type: "text/plain;charset=utf-8" }); +    expect(blob instanceof File).toBe(false); +  }); + +  it("extendable", async () => { +    class Foo extends File { +      constructor(...args) { +        super(...args); +      } + +      bar() { +        return true; +      } + +      text() { +        return super.text(); +      } +    } +    const foo = new Foo(["foo"], "bar.txt", { type: "text/plain;charset=utf-8" }); +    expect(foo instanceof File).toBe(true); +    expect(foo instanceof Blob).toBe(true); +    expect(foo instanceof Object).toBe(true); +    expect(foo instanceof Function).toBe(false); +    expect(foo instanceof Foo).toBe(true); +    expect(foo.bar()).toBe(true); +    expect(foo.name).toBe("bar.txt"); +    expect(foo.type).toBe("text/plain;charset=utf-8"); +    expect(foo.size).toBe(3); +    expect(foo.lastModified).toBe(0); +    expect(await foo.text()).toBe("foo"); +  }); +}); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 834536020..4040f5ce2 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -1748,7 +1748,7 @@ it("constants", () => {  });  it("File", () => { -  expect(BufferModule.File).toBe(Blob); +  expect(BufferModule.File).toBe(File);  });  it("transcode", () => { diff --git a/test/js/web/html/FormData.test.ts b/test/js/web/html/FormData.test.ts index 8c66abb10..c742bd33e 100644 --- a/test/js/web/html/FormData.test.ts +++ b/test/js/web/html/FormData.test.ts @@ -15,9 +15,11 @@ describe("FormData", () => {    it("should be able to append a Blob", async () => {      const formData = new FormData(); -    formData.append("foo", new Blob(["bar"])); +    formData.append("foo", new Blob(["bar"]), "mynameis.txt");      expect(await ((await formData.get("foo")) as Blob)!.text()).toBe("bar");      expect(formData.getAll("foo")[0] instanceof Blob).toBe(true); +    expect(formData.getAll("foo")[0] instanceof File).toBe(true); +    expect((formData.getAll("foo")[0] as File).name).toBe("mynameis.txt");    });    it("should be able to set a Blob", async () => { diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index 46422c210..9b4c86006 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -20,6 +20,7 @@ test("exists", () => {    expect(typeof Blob !== "undefined").toBe(true);    expect(typeof FormData !== "undefined").toBe(true);    expect(typeof Worker !== "undefined").toBe(true); +  expect(typeof File !== "undefined").toBe(true);  });  const globalSetters = [ | 
