| // Copyright 2026 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" |
| |
| #include <gtest/gtest.h> |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <chrono> |
| #include <cstdint> |
| #include <filesystem> |
| #include <fstream> |
| #include <optional> |
| #include <ostream> |
| #include <sstream> |
| #include <string> |
| #include <string_view> |
| #include <thread> |
| #include <vector> |
| |
| #include "src/dawn/common/SystemUtils.h" |
| #include "src/dawn/node/napi_v8/napi_v8.h" |
| #include "src/dawn/node/standalone/EventLoop.h" |
| #include "src/dawn/node/standalone/Polyfills.h" |
| #include "src/dawn/node/test/V8TestEnvironment.h" |
| |
| namespace { |
| |
| // How long the clock tests sleep, and the bounds a reading taken across that sleep must fall |
| // within. The lower bound sits under the sleep to leave room for clock-source rounding; the upper |
| // is loose enough that only a wrong unit, rather than a loaded machine, can reach it. |
| constexpr std::chrono::milliseconds kSleepDuration{50}; |
| constexpr double kMinElapsedMs = 40.0; |
| constexpr double kMaxElapsedMs = 5000.0; |
| |
| // Redirects `stream` into a buffer for the lifetime of this object, so that tests can assert on |
| // the exact bytes a polyfill writes rather than merely that it did not crash. |
| class StreamCapture { |
| public: |
| explicit StreamCapture(std::ostream& stream) : stream_(stream) { |
| original_ = stream_.rdbuf(buffer_.rdbuf()); |
| } |
| |
| ~StreamCapture() { stream_.rdbuf(original_); } |
| |
| StreamCapture(const StreamCapture&) = delete; |
| StreamCapture& operator=(const StreamCapture&) = delete; |
| |
| std::string Str() const { return buffer_.str(); } |
| |
| private: |
| std::ostringstream buffer_; |
| std::ostream& stream_; |
| std::streambuf* original_; |
| }; |
| |
| // A directory of its own for a test that needs real files, removed when the test ends. The name |
| // carries the test that asked for it, and a counter keeps two of them apart. |
| class TempDir { |
| public: |
| TempDir() { |
| static std::atomic<uint32_t> counter{0}; |
| const ::testing::TestInfo* info = ::testing::UnitTest::GetInstance()->current_test_info(); |
| std::filesystem::path base = std::filesystem::temp_directory_path(); |
| for (uint32_t attempt = 0; attempt < kMaxAttempts; ++attempt) { |
| std::filesystem::path candidate = |
| base / |
| ("dawn_polyfills_" + std::string(info->name()) + "_" + std::to_string(counter++)); |
| std::error_code ec; |
| if (std::filesystem::create_directory(candidate, ec)) { |
| path_ = candidate; |
| return; |
| } |
| } |
| ADD_FAILURE() << "no temporary directory could be created under " << base; |
| } |
| |
| ~TempDir() { |
| std::error_code ec; |
| std::filesystem::remove_all(path_, ec); |
| } |
| |
| TempDir(const TempDir&) = delete; |
| TempDir& operator=(const TempDir&) = delete; |
| |
| // Paths handed to JavaScript use forward slashes throughout: Windows accepts them, and unlike |
| // a backslash they do not start an escape sequence when pasted into a string literal. |
| std::string JsPath() const { return path_.generic_string(); } |
| |
| // Creates a file in the directory and returns its path, for JavaScript. |
| std::string WriteFile(const std::string& name, const std::string& contents) const { |
| std::filesystem::path file = path_ / name; |
| std::error_code ec; |
| std::filesystem::create_directories(file.parent_path(), ec); |
| std::ofstream out(file, std::ios::binary); |
| out << contents; |
| out.close(); |
| EXPECT_TRUE(out.good()) << "could not write " << file; |
| return file.generic_string(); |
| } |
| |
| private: |
| static constexpr uint32_t kMaxAttempts = 32; |
| |
| std::filesystem::path path_; |
| }; |
| |
| class PolyfillsTest : public dawn::node::test::V8IsolateTest { |
| protected: |
| PolyfillsTest() : V8IsolateTest(v8::MicrotasksPolicy::kExplicit) {} |
| |
| void SetUp() override { |
| V8IsolateTest::SetUp(); |
| env_ = dawn::napi_v8::CreateEnv(isolate_, context()); |
| loop_.emplace(isolate_, dawn::node::test::V8Platform()); |
| } |
| |
| void TearDown() override { |
| // Before the environment: a task still queued in the loop owns a reference to the |
| // JavaScript callback it was going to call. |
| loop_.reset(); |
| dawn::napi_v8::DestroyEnv(env_); |
| V8IsolateTest::TearDown(); |
| } |
| |
| // The loop the timer polyfills schedule onto. A test drives it by hand with RunOneIteration() |
| // rather than running it, so that nothing depends on how long anything takes. |
| dawn::node::standalone::EventLoop& loop() { return *loop_; } |
| |
| napi_value RunScript(const std::string& code) { |
| napi_value script_src; |
| napi_value result; |
| napi_status status = |
| napi_create_string_utf8(env_, code.c_str(), code.length(), &script_src); |
| EXPECT_EQ(status, napi_ok); |
| status = napi_run_script(env_, script_src, &result); |
| EXPECT_EQ(status, napi_ok); |
| return result; |
| } |
| |
| void RunScriptAllowingExit(const std::string& code) { |
| napi_value script_src; |
| napi_value result; |
| ASSERT_EQ(napi_create_string_utf8(env_, code.c_str(), code.length(), &script_src), napi_ok); |
| napi_run_script(env_, script_src, &result); |
| isolate_->CancelTerminateExecution(); |
| } |
| |
| void RunMicrotasks() { isolate_->PerformMicrotaskCheckpoint(); } |
| |
| // Whether evaluating `expression` throws an Error. RunScript() expects the script itself to |
| // succeed, so the throw has to be caught in JavaScript rather than escaping to the fixture. |
| bool Throws(const std::string& expression) { |
| return ToBool(RunScript("(function() { try { " + expression + |
| "; return false; } catch (e) { return e instanceof Error; } })()")); |
| } |
| |
| std::string ToString(napi_value value) { |
| size_t length = 0; |
| EXPECT_EQ(napi_get_value_string_utf8(env_, value, nullptr, 0, &length), napi_ok); |
| std::vector<char> buffer(length + 1); |
| EXPECT_EQ(napi_get_value_string_utf8(env_, value, buffer.data(), buffer.size(), &length), |
| napi_ok); |
| return std::string(buffer.data(), length); |
| } |
| |
| bool ToBool(napi_value value) { |
| bool result = false; |
| EXPECT_EQ(napi_get_value_bool(env_, value, &result), napi_ok); |
| return result; |
| } |
| |
| uint32_t ToUint32(napi_value value) { |
| uint32_t result = 0; |
| EXPECT_EQ(napi_get_value_uint32(env_, value, &result), napi_ok); |
| return result; |
| } |
| |
| double ToDouble(napi_value value) { |
| double result = 0.0; |
| EXPECT_EQ(napi_get_value_double(env_, value, &result), napi_ok); |
| return result; |
| } |
| |
| napi_env env_ = nullptr; |
| |
| private: |
| // Held by value once SetUp() has an isolate to give it. |
| std::optional<dawn::node::standalone::EventLoop> loop_; |
| }; |
| |
| TEST_F(PolyfillsTest, ConsoleGlobals) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE( |
| ToBool(RunScript("typeof console.log === 'function' && " |
| "typeof console.warn === 'function' && " |
| "typeof console.error === 'function' && " |
| "typeof console.info === 'function' && " |
| "typeof console.debug === 'function'"))); |
| } |
| |
| TEST_F(PolyfillsTest, ConsoleLogInfoAndDebugWriteLinesToStdout) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| StreamCapture out(std::cout); |
| StreamCapture log(std::clog); |
| StreamCapture err(std::cerr); |
| RunScript("console.log('one'); console.info('two'); console.debug('three');"); |
| |
| // All three are unprefixed stdout writes, one line each. |
| EXPECT_EQ(out.Str(), "one\ntwo\nthree\n"); |
| EXPECT_EQ(log.Str(), ""); |
| EXPECT_EQ(err.Str(), ""); |
| } |
| |
| TEST_F(PolyfillsTest, ConsoleWarnAndErrorAreLabelled) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| StreamCapture out(std::cout); |
| StreamCapture log(std::clog); |
| StreamCapture err(std::cerr); |
| RunScript("console.warn('careful'); console.error('broken');"); |
| |
| EXPECT_EQ(out.Str(), ""); |
| EXPECT_EQ(log.Str(), "[WARN] careful\n"); |
| EXPECT_EQ(err.Str(), "[ERROR] broken\n"); |
| } |
| |
| TEST_F(PolyfillsTest, ConsoleJoinsArgumentsWithSpaces) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| StreamCapture out(std::cout); |
| RunScript("console.log('a', 1, true, null, undefined, {}, [1, 2]);"); |
| RunScript("console.log();"); |
| |
| EXPECT_EQ(out.Str(), "a 1 true null undefined [object Object] 1,2\n\n"); |
| } |
| |
| TEST_F(PolyfillsTest, FsExistsSync) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("present.txt", "x"); |
| |
| EXPECT_TRUE(ToBool(RunScript("_fs_polyfill.existsSync('" + file + "')"))); |
| EXPECT_TRUE(ToBool(RunScript("_fs_polyfill.existsSync('" + dir.JsPath() + "')"))); |
| EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync('" + dir.JsPath() + "/absent.txt')"))); |
| |
| // Anything that is not a path answers false rather than throwing. |
| EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync(42)"))); |
| EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync()"))); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileSyncDecodesWithAnEncoding) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU\n"); |
| |
| // The encoding may be given on its own or inside an options object. |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), |
| "Hello WebGPU\n"); |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', {encoding: 'utf8'})")), |
| "Hello WebGPU\n"); |
| // All buffer encodings are case-insensitive. |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'UTF-8')")), |
| "Hello WebGPU\n"); |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', {encoding: 'Utf8'})")), |
| "Hello WebGPU\n"); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileSyncReturnsBytesWithoutAnEncoding) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| // Bytes that are neither printable nor valid UTF-8, so a decoded read could not reproduce |
| // them. |
| std::string file = dir.WriteFile("bytes.bin", std::string("\x01\x02\x00\xff", 4)); |
| |
| EXPECT_TRUE( |
| ToBool(RunScript("_fs_polyfill.readFileSync('" + file + "') instanceof Uint8Array"))); |
| EXPECT_EQ( |
| ToString(RunScript("Array.from(_fs_polyfill.readFileSync('" + file + "')).join(',')")), |
| "1,2,0,255"); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileSyncThrowsForAMissingFile) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| |
| EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + dir.JsPath() + "/absent.txt')")); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileSyncPreservesNewlinesVerbatim) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("crlf.txt", "a\r\nb\n"); |
| |
| // The file is read as bytes whether or not it is decoded, so a CR survives. Opening in text |
| // mode on Windows would drop it and leave us disagreeing with Node. |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), "a\r\nb\n"); |
| EXPECT_EQ( |
| ToString(RunScript("Array.from(_fs_polyfill.readFileSync('" + file + "')).join(',')")), |
| "97,13,10,98,10"); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileSyncHandlesAnEmptyFile) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("empty.txt", ""); |
| |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), ""); |
| EXPECT_EQ(ToUint32(RunScript("_fs_polyfill.readFileSync('" + file + "').length")), 0u); |
| } |
| |
| // An option the polyfill cannot honour has to be reported, not ignored: quietly returning |
| // something that does not match what was asked for is worse than failing. |
| TEST_F(PolyfillsTest, FsRejectsUnsupportedOptions) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU"); |
| |
| EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', {flag: 'r'})")); |
| EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 'latin1')")); |
| EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 42)")); |
| EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', {withFileTypes: true})")); |
| EXPECT_TRUE(Throws("_fs_polyfill.statSync('" + file + "', {bigint: true})")); |
| |
| // An absent, undefined or null options argument is not an unsupported one. |
| EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "')")); |
| EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "', undefined)")); |
| EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "', {encoding: null})")); |
| EXPECT_FALSE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', 'utf8')")); |
| } |
| |
| // Node's readdirSync() answers with Buffers under the 'buffer' encoding, and its readFileSync() |
| // rejects that encoding outright. We reject it in both: there is no Buffer in this runtime, and |
| // handing back Uint8Arrays instead would break the first caller to call toString() on one. |
| TEST_F(PolyfillsTest, FsRejectsTheBufferEncoding) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU"); |
| |
| EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', 'buffer')")); |
| EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', {encoding: 'buffer'})")); |
| EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 'buffer')")); |
| |
| // Bytes are still reachable from readFileSync, just by omitting the encoding rather than by |
| // naming one, which is what Node does too. |
| EXPECT_TRUE( |
| ToBool(RunScript("_fs_polyfill.readFileSync('" + file + "') instanceof Uint8Array"))); |
| } |
| |
| TEST_F(PolyfillsTest, FsRejectsUncPaths) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // Node accepts UNC paths; this runtime deliberately refuses them, because Node and |
| // std::filesystem disagree about where a UNC root ends. String.raw keeps the backslashes away |
| // from JavaScript's escape rules so the path reaches the polyfill exactly as written. |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.readFileSync(String.raw`\\server\share\a`))")); |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.readdirSync(String.raw`\\server\share`))")); |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.statSync(String.raw`\\server\share\a`))")); |
| |
| // existsSync() answers false rather than throwing for arguments it cannot use, but a UNC path |
| // is not a question about whether something exists, so it is refused rather than reported |
| // absent. |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\server\share\a`))")); |
| |
| // The device forms share the prefix and go the same way. |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\?\C:\a`))")); |
| EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\.\PhysicalDrive0`))")); |
| } |
| |
| TEST_F(PolyfillsTest, FsAcceptsPathsThatMerelyResembleUnc) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU"); |
| |
| // It takes two leading separators to make a UNC path. One is not enough, and a backslash |
| // anywhere else is just a character. None of these exist, but none of them are refused |
| // either - the distinction being that a refusal throws. |
| EXPECT_FALSE(ToBool(RunScript(R"(_fs_polyfill.existsSync(String.raw`\server\share`))"))); |
| EXPECT_FALSE(ToBool(RunScript(R"(_fs_polyfill.existsSync('a\\b'))"))); |
| |
| // Where the two platforms part company. On Windows "//server/share" is UNC and refused; on |
| // POSIX a leading "//" is a legal path that the path module already handles correctly, so it |
| // is left alone. |
| #if defined(_WIN32) |
| EXPECT_TRUE(Throws("_fs_polyfill.existsSync('//no/such/path')")); |
| #else |
| EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync('//no/such/path')"))); |
| #endif |
| |
| // And an ordinary path is untouched by any of this. |
| EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), |
| "Hello WebGPU"); |
| } |
| |
| TEST_F(PolyfillsTest, FsReaddirSync) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| dir.WriteFile("a.txt", "a"); |
| dir.WriteFile("b.txt", "b"); |
| |
| // The entries are names, not paths, and the order is the filesystem's. |
| EXPECT_EQ( |
| ToString(RunScript("_fs_polyfill.readdirSync('" + dir.JsPath() + "').sort().join(',')")), |
| "a.txt,b.txt"); |
| } |
| |
| TEST_F(PolyfillsTest, FsStatSync) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("file.txt", "x"); |
| |
| EXPECT_TRUE(ToBool(RunScript("const f = _fs_polyfill.statSync('" + file + |
| "'); f.isFile() && !f.isDirectory()"))); |
| EXPECT_TRUE(ToBool(RunScript("const d = _fs_polyfill.statSync('" + dir.JsPath() + |
| "'); d.isDirectory() && !d.isFile()"))); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileCallbackRunsAsynchronously) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU"); |
| |
| RunScript( |
| "globalThis.error = 'unset'; globalThis.data = 'unset';" |
| "_fs_polyfill.readFile('" + |
| file + "', 'utf8', (err, data) => { globalThis.error = err; globalThis.data = data; });"); |
| |
| // Nothing has happened yet: that the callback waits for the microtask checkpoint is what |
| // makes this the asynchronous form. |
| EXPECT_EQ(ToString(RunScript("globalThis.data")), "unset"); |
| |
| RunMicrotasks(); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.error === null"))); |
| EXPECT_EQ(ToString(RunScript("globalThis.data")), "Hello WebGPU"); |
| } |
| |
| TEST_F(PolyfillsTest, FsReadFileCallbackReportsFailure) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| |
| // With the encoding omitted the callback takes the second argument position. |
| RunScript( |
| "globalThis.error = 'unset';" |
| "_fs_polyfill.readFile('" + |
| dir.JsPath() + "/absent.txt', (err) => { globalThis.error = err; });"); |
| |
| RunMicrotasks(); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.error instanceof Error"))); |
| } |
| |
| TEST_F(PolyfillsTest, FsPromisesResolve) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string file = dir.WriteFile("hello.txt", "Hello WebGPU"); |
| |
| RunScript( |
| "globalThis.data = 'unset';" |
| "_fs_polyfill.promises.readFile('" + |
| file + "', 'utf8').then((d) => { globalThis.data = d; });"); |
| EXPECT_EQ(ToString(RunScript("globalThis.data")), "unset"); |
| |
| RunMicrotasks(); |
| EXPECT_EQ(ToString(RunScript("globalThis.data")), "Hello WebGPU"); |
| |
| RunScript( |
| "globalThis.entries = 'unset';" |
| "_fs_polyfill.promises.readdir('" + |
| dir.JsPath() + "').then((e) => { globalThis.entries = e.join(','); });"); |
| RunMicrotasks(); |
| EXPECT_EQ(ToString(RunScript("globalThis.entries")), "hello.txt"); |
| |
| RunScript( |
| "globalThis.isFile = 'unset';" |
| "_fs_polyfill.promises.stat('" + |
| file + "').then((s) => { globalThis.isFile = s.isFile(); });"); |
| RunMicrotasks(); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.isFile"))); |
| } |
| |
| TEST_F(PolyfillsTest, FsPromisesRejectWhenTheSyncCallThrows) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| |
| RunScript( |
| "globalThis.reason = 'unset';" |
| "_fs_polyfill.promises.stat('" + |
| dir.JsPath() + "/absent').catch((e) => { globalThis.reason = e; });"); |
| |
| RunMicrotasks(); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.reason instanceof Error"))); |
| } |
| |
| // The path module answers in the host's separator, so the expectations below are written in their |
| // POSIX spelling and translated. dirname() is the exception: Node slices its answer out of the |
| // input, so whatever separator was written comes back unchanged. |
| std::string Native(std::string_view posix) { |
| std::string native(posix); |
| #if defined(_WIN32) |
| std::replace(native.begin(), native.end(), '/', '\\'); |
| #endif |
| return native; |
| } |
| |
| // A rooted path that names no drive is resolved against the drive of the working directory, as it |
| // is in Node, so an absolute expectation has to carry that drive on Windows. |
| std::string RootedNative(std::string_view posix) { |
| return std::filesystem::current_path().root_name().string() + Native(posix); |
| } |
| |
| TEST_F(PolyfillsTest, PathNormalize) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/a/b/../c/./d')")), Native("/a/c/d")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a//b')")), Native("a/b")); |
| |
| // A trailing slash says the path names a directory, so it is kept. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a//b/')")), Native("a/b/")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/a/b/')")), Native("/a/b/")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('./')")), Native("./")); |
| |
| // The input is what decides that, and only the input: lexically_normal() invents a trailing |
| // separator of its own whenever the last component was a dot segment, and it has to be |
| // discarded. Without that, the first of these answers 'a/'. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a/b/..')")), Native("a")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a/b/../')")), Native("a/")); |
| |
| // A '..' that cannot be cancelled is kept in a relative path but dropped at the root. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('../../a')")), Native("../../a")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/..')")), Native("/")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('')")), "."); |
| |
| #if !defined(_WIN32) |
| // A leading run of separators collapses to one. std::filesystem keeps it, because POSIX |
| // reserves "//" for the implementation. POSIX-only: on Windows this shape is UNC. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('//a/b')")), "/a/b"); |
| #endif |
| } |
| |
| TEST_F(PolyfillsTest, PathJoin) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b', 'c')")), Native("a/b/c")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/a', 'b/', '../c')")), Native("/a/c")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', '', 'b')")), Native("a/b")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('')")), "."); |
| |
| // join() concatenates unconditionally, where operator/ would let a rooted argument discard |
| // everything before it and answer '/b'. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/a', '/b')")), Native("/a/b")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/', 'a')")), Native("/a")); |
| |
| // join() normalizes, so the trailing slash of the last argument survives, and a '..' argument |
| // does not leave one behind. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b/')")), Native("a/b/")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b', '..')")), Native("a")); |
| } |
| |
| TEST_F(PolyfillsTest, PathDirname) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // The answer is a slice of the argument, so it comes back in the separator it was written in |
| // on either platform. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a/b/c.js')")), "/a/b"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a')")), "/"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('a.js')")), "."); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('')")), "."); |
| |
| // Unlike normalize(), dirname() ignores a trailing slash rather than reading it as a segment |
| // boundary, so these name the parent of b, not b itself. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a/b/')")), "/a"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('a/')")), "."); |
| |
| // And it does not normalize what it keeps, which is why it cannot be parent_path(): that |
| // would answer '/a'. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a//b')")), "/a/"); |
| |
| #if !defined(_WIN32) |
| // Nothing but separators names the root, and a leading "//" is kept whole. POSIX-only: on |
| // Windows two leading separators are UNC, which is refused rather than answered. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('///')")), "/"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('//a')")), "//"); |
| #endif |
| } |
| |
| TEST_F(PolyfillsTest, PathResolve) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', 'c')")), RootedNative("/a/b/c")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', '../c')")), RootedNative("/a/c")); |
| |
| // An absolute argument discards everything to its left, and a call with no absolute argument |
| // at all is taken relative to the working directory. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', '/c')")), RootedNative("/c")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('a')")), |
| (std::filesystem::current_path() / "a").string()); |
| |
| // Unlike normalize(), resolve() never answers with a trailing separator, whether the argument |
| // carried one or lexically_normal() invented one. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b/')")), RootedNative("/a/b")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b/..')")), RootedNative("/a")); |
| |
| // An empty argument is skipped rather than read as '.', and a call with nothing left is the |
| // working directory. std::filesystem::absolute('') fails outright. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a', '')")), RootedNative("/a")); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('')")), |
| std::filesystem::current_path().string()); |
| } |
| |
| TEST_F(PolyfillsTest, PathRelative) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b', '/a/b/c/d')")), Native("c/d")); |
| |
| // Identical paths are "" and not ".", which is what lexically_relative() answers. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b', '/a/b')")), ""); |
| |
| // Leaving the common prefix behind takes one '..' per remaining segment of `from`. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b/c', '/a/d')")), Native("../../d")); |
| |
| // A zero-length side means the working directory, because resolve() is what defines both ends. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('', 'a')")), "a"); |
| } |
| |
| TEST_F(PolyfillsTest, PathRejectsUncPaths) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // The same refusal the fs entry points make, for the same reason: Node and std::filesystem |
| // disagree about where a UNC root ends. Any argument can carry the prefix, not just the first. |
| EXPECT_TRUE(Throws(R"(_path_polyfill.normalize(String.raw`\\server\share\a`))")); |
| EXPECT_TRUE(Throws(R"(_path_polyfill.dirname(String.raw`\\server\share\a`))")); |
| EXPECT_TRUE(Throws(R"(_path_polyfill.join('a', String.raw`\\server\share`))")); |
| EXPECT_TRUE(Throws(R"(_path_polyfill.resolve(String.raw`\\server\share`))")); |
| EXPECT_TRUE(Throws(R"(_path_polyfill.relative('a', String.raw`\\?\C:\a`))")); |
| } |
| |
| #if defined(_WIN32) |
| // The shapes that have no POSIX spelling to translate. Every expectation here is what |
| // path.win32 answers. |
| TEST_F(PolyfillsTest, PathDriveLetters) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // A drive with no root directory names the working directory on that drive, and Node spells |
| // the implied here-ness out rather than leaving the bare drive. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:')")), "C:."); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:.')")), "C:."); |
| |
| // Either separator is accepted on the way in; the preferred one comes back out. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:/a/b/')")), "C:\\a\\b\\"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:/a/b/..')")), "C:\\a"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.join('C:/a', 'b/')")), "C:\\a\\b\\"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('C:/a', 'b')")), "C:\\a\\b"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('C:/a/b', 'C:/a/c')")), "..\\c"); |
| |
| // Except from dirname(), which slices the argument and so keeps what was written. The drive |
| // is the root here, and is never cut into. |
| EXPECT_EQ(ToString(RunScript(R"(_path_polyfill.dirname(String.raw`C:\a\b`))")), "C:\\a"); |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('C:/a//b')")), "C:/a/"); |
| EXPECT_EQ(ToString(RunScript(R"(_path_polyfill.dirname(String.raw`C:\a`))")), "C:\\"); |
| |
| // Two drives are unrelated, so there is no path from one to the other and the answer is just |
| // the destination. lexically_relative() says "" for that, which would read as "the same |
| // place". Purely lexical, so the drives need not exist. |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('C:/a', 'D:/b')")), "D:\\b"); |
| } |
| #endif |
| |
| TEST_F(PolyfillsTest, PathRejectsArgumentsThatAreNotStrings) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(Throws("_path_polyfill.normalize()")); |
| EXPECT_TRUE(Throws("_path_polyfill.normalize(5)")); |
| EXPECT_TRUE(Throws("_path_polyfill.join('a', null)")); |
| EXPECT_TRUE(Throws("_path_polyfill.relative('a')")); |
| } |
| |
| TEST_F(PolyfillsTest, PathSep) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("_path_polyfill.sep")), Native("/")); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessArgvAndCwd) { |
| dawn::node::standalone::PolyfillOptions options; |
| options.argv = {"runner", "arg1", "arg2"}; |
| dawn::node::standalone::RegisterPolyfills(env_, loop(), options); |
| |
| EXPECT_EQ(ToUint32(RunScript("process.argv.length")), 3u); |
| EXPECT_EQ(ToString(RunScript("process.argv.join(',')")), "runner,arg1,arg2"); |
| |
| // cwd() must report the real working directory, not merely something of type string. |
| EXPECT_EQ(ToString(RunScript("process.cwd()")), std::filesystem::current_path().string()); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessArgvIsEmptyByDefault) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(ToBool(RunScript("Array.isArray(process.argv)"))); |
| EXPECT_EQ(ToUint32(RunScript("process.argv.length")), 0u); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessEnvExposesDawnFlags) { |
| dawn::ScopedEnvironmentVar dawn_flags("DAWN_FLAGS", "--a-flag"); |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("process.env.DAWN_FLAGS")), "--a-flag"); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessEnvIsEmptyWithoutDawnFlags) { |
| dawn::ScopedEnvironmentVar dawn_flags("DAWN_FLAGS", nullptr); |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // `process.env` is not the real environment: only the variables the runner reads are copied |
| // into it, so with DAWN_FLAGS unset it is empty. |
| EXPECT_EQ(ToUint32(RunScript("Object.keys(process.env).length")), 0u); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessExitStopsTheLoop) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScriptAllowingExit("process.exit(42)"); |
| EXPECT_TRUE(loop().stopped()); |
| EXPECT_EQ(loop().exit_code(), 42); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessExitDefaultsToZero) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScriptAllowingExit("process.exit()"); |
| EXPECT_TRUE(loop().stopped()); |
| EXPECT_EQ(loop().exit_code(), 0); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessExitWithACodeThatIsNotANumberExitsWithZero) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScriptAllowingExit("process.exit('not a number')"); |
| EXPECT_TRUE(loop().stopped()); |
| EXPECT_EQ(loop().exit_code(), 0); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessStreamWritesAreVerbatim) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| StreamCapture out(std::cout); |
| StreamCapture log(std::clog); |
| RunScript("process.stdout.write('no'); process.stdout.write(' newline');"); |
| RunScript("process.stderr.write('unlabelled');"); |
| |
| // Unlike console.log(), write() adds neither a newline nor a prefix. |
| EXPECT_EQ(out.Str(), "no newline"); |
| EXPECT_EQ(log.Str(), "unlabelled"); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessStreamWriteReturnsTrue) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| StreamCapture out(std::cout); |
| StreamCapture log(std::clog); |
| |
| // write() reports whether the caller may write again immediately, rather than waiting for a |
| // 'drain' event. |
| EXPECT_TRUE(ToBool(RunScript("process.stdout.write('x')"))); |
| EXPECT_TRUE(ToBool(RunScript("process.stderr.write('x')"))); |
| } |
| |
| // process.exit() must not return to JavaScript: once it is called, no further JavaScript runs, |
| // JavaScript cannot catch the unwind, and a later exit() cannot overwrite the exit code. |
| TEST_F(PolyfillsTest, ProcessExitDoesNotReturnToJavaScript) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript("globalThis._log = []; globalThis._record = (s) => { globalThis._log.push(s); };"); |
| RunScriptAllowingExit(R"( |
| try { |
| process.exit(3); |
| _record('after-exit'); |
| } catch (e) { |
| _record('caught'); |
| } finally { |
| _record('finally'); |
| } |
| _record('after-try'); |
| process.exit(0); |
| )"); |
| |
| // The second exit() was never reached, so the first exit code stands, and nothing after the |
| // exit() ran - not even the `finally` block, which JavaScript would otherwise always run. |
| EXPECT_TRUE(loop().stopped()); |
| EXPECT_EQ(loop().exit_code(), 3); |
| EXPECT_EQ(ToString(RunScript("globalThis._log.join(',')")), ""); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessExitFromMicrotaskDoesNotReturnToJavaScript) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._log = []; |
| globalThis._record = (s) => { globalThis._log.push(s); }; |
| Promise.resolve().then(() => { |
| process.exit(3); |
| _record('after-exit'); |
| }); |
| Promise.resolve().then(() => { _record('later-microtask'); }); |
| )"); |
| |
| RunMicrotasks(); |
| isolate_->CancelTerminateExecution(); |
| |
| // Neither the rest of the microtask that exited nor the microtask queued behind it ran. |
| EXPECT_TRUE(loop().stopped()); |
| EXPECT_EQ(loop().exit_code(), 3); |
| EXPECT_EQ(ToString(RunScript("globalThis._log.join(',')")), ""); |
| } |
| |
| TEST_F(PolyfillsTest, EventGlobals) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE( |
| ToBool(RunScript("typeof EventTarget === 'function' && " |
| "typeof Event === 'function' && " |
| "typeof CustomEvent === 'function' && " |
| "typeof MessageEvent === 'function' && " |
| "typeof DOMException === 'function'"))); |
| |
| // EventTarget dispatches to both function listeners and { handleEvent } object listeners. |
| EXPECT_EQ(ToString(RunScript(R"( |
| const target = new EventTarget(); |
| const log = []; |
| target.addEventListener('test', function(e) { |
| log.push('fn:' + (this === target) + ':' + e.type); |
| }); |
| target.addEventListener('test', { |
| tag: 'obj', |
| handleEvent(e) { |
| log.push(this.tag + ':' + e.type); |
| }, |
| }); |
| target.dispatchEvent(new Event('test')); |
| log.join(',') |
| )")), |
| "fn:true:test,obj:test"); |
| } |
| |
| TEST_F(PolyfillsTest, DOMException) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript(R"( |
| const named = new DOMException('bad state', 'InvalidStateError'); |
| const unnamed = new DOMException('failed'); |
| [ |
| named instanceof DOMException, |
| named instanceof Error, |
| named.name, |
| named.message, |
| unnamed.name, |
| ].join(',') |
| )")), |
| "true,true,InvalidStateError,bad state,Error"); |
| } |
| |
| TEST_F(PolyfillsTest, EventCancellation) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._target = new EventTarget(); |
| globalThis._target.addEventListener('err', (e) => { e.preventDefault(); }); |
| globalThis._target.addEventListener('ok', () => {}); |
| )"); |
| |
| // A cancelable event is cancelled when preventDefault() is called. |
| napi_value res = RunScript(R"( |
| const cancelable = new Event('err', { cancelable: true }); |
| [globalThis._target.dispatchEvent(cancelable), cancelable.defaultPrevented].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "false,true"); |
| |
| // An event that is not cancelable ignores preventDefault(). |
| res = RunScript(R"( |
| const plain = new Event('err'); |
| [globalThis._target.dispatchEvent(plain), plain.defaultPrevented].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "true,false"); |
| |
| // A cancelable event where preventDefault() is not called remains uncancelled. |
| res = RunScript(R"( |
| const uncancelled = new Event('ok', { cancelable: true }); |
| [globalThis._target.dispatchEvent(uncancelled), uncancelled.defaultPrevented].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "true,false"); |
| } |
| |
| TEST_F(PolyfillsTest, EventListenerOnce) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript(R"( |
| const target = new EventTarget(); |
| let once = 0; |
| let always = 0; |
| target.addEventListener('x', () => { once++; }, { once: true }); |
| target.addEventListener('x', () => { always++; }); |
| target.dispatchEvent(new Event('x')); |
| target.dispatchEvent(new Event('x')); |
| [once, always].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "1,2"); |
| } |
| |
| TEST_F(PolyfillsTest, EventTargetDeduplicatesByTypeAndCapture) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript(R"( |
| const target = new EventTarget(); |
| const log = []; |
| const fn = () => { log.push('fn'); }; |
| const other = () => { log.push('other'); }; |
| target.addEventListener('x', fn); |
| target.addEventListener('x', other); |
| target.addEventListener('x', fn); |
| target.addEventListener('x', fn, false); |
| target.addEventListener('x', fn, { capture: false }); |
| target.addEventListener('x', fn, true); |
| target.addEventListener('x', fn, { capture: true }); |
| target.dispatchEvent(new Event('x')); |
| const afterBoth = log.join(','); |
| |
| log.length = 0; |
| target.removeEventListener('x', fn, false); |
| target.dispatchEvent(new Event('x')); |
| const afterRemoveBubble = log.join(','); |
| |
| log.length = 0; |
| target.removeEventListener('x', fn, { capture: true }); |
| target.dispatchEvent(new Event('x')); |
| const afterRemoveCapture = log.join(','); |
| |
| [afterBoth, afterRemoveBubble, afterRemoveCapture].join(' | ') |
| )"); |
| EXPECT_EQ(ToString(res), "fn,other,fn | other,fn | other"); |
| } |
| |
| TEST_F(PolyfillsTest, CustomEvent) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript(R"( |
| const target = new EventTarget(); |
| let seen = null; |
| target.addEventListener('bar', (e) => { seen = e; }, { once: true }); |
| target.dispatchEvent(new CustomEvent('bar')); |
| [seen instanceof CustomEvent, seen instanceof Event, seen.type].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "true,true,bar"); |
| } |
| |
| TEST_F(PolyfillsTest, MessageEvent) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript(R"( |
| const withData = new MessageEvent('import', { data: { url: 'a.spec.js' } }); |
| const withoutData = new MessageEvent('finish'); |
| [ |
| withData instanceof MessageEvent, |
| withData instanceof Event, |
| withData.type, |
| withData.data.url, |
| withoutData.data === undefined, |
| ].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "true,true,import,a.spec.js,true"); |
| } |
| |
| TEST_F(PolyfillsTest, ProcessVersions) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript( |
| "String(typeof process.versions === 'object' && " |
| "typeof process.versions.node === 'string' && process.versions.node.length > 0)"); |
| EXPECT_EQ(ToString(res), "true"); |
| } |
| |
| TEST_F(PolyfillsTest, TextEncoder) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| // Non-ASCII input covers all of the UTF-8 sequence lengths: two bytes for e-acute, three for |
| // the arrow and four for the surrogate pair. |
| napi_value res = RunScript(R"( |
| const bytes = new TextEncoder().encode('h\u00e9llo \u2192 \ud83c\udf0d'); |
| Array.from(bytes).join(' ') |
| )"); |
| EXPECT_EQ(ToString(res), "104 195 169 108 108 111 32 226 134 146 32 240 159 140 141"); |
| |
| // Undefined input encodes to an empty Uint8Array, and an unpaired surrogate is replaced with |
| // U+FFFD (EF BF BD) rather than throwing. |
| res = RunScript(R"( |
| [new TextEncoder().encode().length, |
| Array.from(new TextEncoder().encode('\ud800')).join(' ')].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "0,239 191 189"); |
| } |
| |
| TEST_F(PolyfillsTest, RequireBuiltins) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| napi_value res = RunScript(R"( |
| globalThis._webgpu_module = { marker: 'wgpu' }; |
| [ |
| require('fs') === globalThis._fs_polyfill, |
| require('node:fs') === globalThis._fs_polyfill, |
| require('path') === globalThis._path_polyfill, |
| require('node:path') === globalThis._path_polyfill, |
| require('process') === globalThis.process, |
| require('node:process') === globalThis.process, |
| require('perf_hooks').performance === globalThis.performance, |
| require('node:perf_hooks').performance === globalThis.performance, |
| require('./dawn.node').marker, |
| require('/any/dir/dawn.node').marker, |
| ].join(',') |
| )"); |
| EXPECT_EQ(ToString(res), "true,true,true,true,true,true,true,true,wgpu,wgpu"); |
| } |
| |
| TEST_F(PolyfillsTest, RequireModules) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| // `sub/leaf.cjs` records the `__dirname` and `__filename` arguments passed to its module |
| // wrapper function so we can verify relative resolution from `sub/main.js`. Backslashes are |
| // normalized to '/' so the suffix check holds on Windows too. |
| dir.WriteFile("sub/leaf.cjs", R"( |
| exports.dir = __dirname.replace(/\\/g, '/'); |
| exports.file = __filename.replace(/\\/g, '/'); |
| )"); |
| // `sub/main.js` increments a global counter each time its body executes and re-exports `leaf`. |
| std::string main_mod = dir.WriteFile("sub/main.js", R"( |
| globalThis.loadCount = (globalThis.loadCount || 0) + 1; |
| module.exports = require('./leaf.cjs'); |
| )"); |
| |
| // Require `sub/main.js` twice: the first call executes `main.js` and `leaf.cjs`, and the second |
| // call must return the cached `exports` object without executing `main.js` a second time. |
| RunScript("globalThis.first = require('" + main_mod + "'); globalThis.second = require('" + |
| main_mod + "');"); |
| |
| EXPECT_TRUE(ToBool(RunScript("globalThis.first.dir.endsWith('/sub')"))); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.first.file.endsWith('/sub/leaf.cjs')"))); |
| EXPECT_TRUE(ToBool(RunScript("globalThis.first === globalThis.second"))); |
| EXPECT_EQ(ToUint32(RunScript("globalThis.loadCount")), 1u); |
| } |
| |
| // `LoadModule` inserts the module into `module_cache` before running its body so a circular |
| // `require()` receives the in-progress `exports` object instead of recursing infinitely. |
| TEST_F(PolyfillsTest, RequireCircularDependency) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| // 1. `cycle_a.js` sets `exports.stage = 'a-init'`, then pauses to require `cycle_b.js`. |
| // 2. `cycle_b.js` requires `cycle_a.js` while `cycle_a.js` is still running, so `cycle_b.js` |
| // observes the in-progress `a.stage` value ('a-init'). |
| // 3. `cycle_a.js` resumes and overwrites `exports.stage = 'a-done'`. |
| std::string cycle_a = dir.WriteFile("cycle_a.js", R"( |
| exports.stage = 'a-init'; |
| const b = require('./cycle_b.js'); |
| exports.seenByB = b.fromA; |
| exports.stage = 'a-done'; |
| )"); |
| dir.WriteFile("cycle_b.js", R"( |
| const a = require('./cycle_a.js'); |
| exports.fromA = a.stage; |
| )"); |
| |
| RunScript("globalThis.cycle = require('" + cycle_a + "');"); |
| EXPECT_EQ(ToString(RunScript("globalThis.cycle.seenByB")), "a-init"); |
| EXPECT_EQ(ToString(RunScript("globalThis.cycle.stage")), "a-done"); |
| } |
| |
| TEST_F(PolyfillsTest, RequireRejectsInvalidAndBareSpecifiers) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| dir.WriteFile("hello/t.txt", "module.exports = 'should not load';"); |
| |
| EXPECT_TRUE(Throws("require()")); |
| EXPECT_TRUE(Throws("require(42)")); |
| EXPECT_TRUE(Throws("require('ansi-colors')")); |
| EXPECT_TRUE(Throws("require('hello/t.txt')")); |
| EXPECT_TRUE(Throws(R"(require(String.raw`\\server\share\a.js`))")); |
| EXPECT_TRUE(Throws("require('" + dir.JsPath() + "/absent.js')")); |
| } |
| |
| // `LoadModule` caches a module's `exports` object before compiling and running its body (to |
| // support circular dependencies). If compilation or execution fails, that entry must be erased from |
| // `module_cache`; otherwise a second `require()` of the same path would hit the cache and return |
| // the half-initialized `exports` object without throwing. |
| TEST_F(PolyfillsTest, RequireDoesNotCacheFailedModules) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string bad_syntax = dir.WriteFile("bad_syntax.js", "const = ;"); |
| std::string runtime_throw = dir.WriteFile("runtime_throw.js", R"( |
| exports.partial = true; |
| throw new Error('init failed'); |
| )"); |
| |
| // First call fails to compile; second call verifies the failed module was not left in the |
| // cache. |
| EXPECT_TRUE(Throws("require('" + bad_syntax + "')")); |
| EXPECT_TRUE(Throws("require('" + bad_syntax + "')")); |
| |
| // First call throws mid-execution after mutating `exports.partial`; second call verifies the |
| // partial `exports` object was evicted from the cache and throws again. |
| EXPECT_TRUE(Throws("require('" + runtime_throw + "')")); |
| EXPECT_TRUE(Throws("require('" + runtime_throw + "')")); |
| } |
| |
| // require() compiles modules with v8::ScriptCompiler::CompileFunction() so stack traces preserve |
| // the source file's 1-based line and column numbers. |
| TEST_F(PolyfillsTest, RequireStackTraceLineAndColumn) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| TempDir dir; |
| std::string thrower = dir.WriteFile("thrower.js", "\n\n throw new Error('boom');"); |
| std::string first = dir.WriteFile("first.js", "throw new Error('boom');"); |
| |
| napi_value res = RunScript( |
| "let thrown = 'no throw';" |
| "try { require('" + |
| thrower + |
| "'); } catch (e) { thrown = e.stack.match(/thrower\\.js:[0-9]+:[0-9]+/)[0]; }" |
| "thrown"); |
| EXPECT_EQ(ToString(res), "thrower.js:3:9"); |
| |
| res = RunScript( |
| "let firstThrown = 'no throw';" |
| "try { require('" + |
| first + |
| "'); } catch (e) { firstThrown = e.stack.match(/first\\.js:[0-9]+:[0-9]+/)[0]; }" |
| "firstThrown"); |
| EXPECT_EQ(ToString(res), "first.js:1:7"); |
| } |
| |
| TEST_F(PolyfillsTest, PerformanceNow) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("typeof performance.now()")), "number"); |
| |
| double now = ToDouble(RunScript("performance.now()")); |
| EXPECT_GE(now, 0.0); |
| // now() counts from registration, so a test that has only just started cannot be minutes in. |
| // A clock returning, say, milliseconds since the epoch would fail here. |
| EXPECT_LT(now, 60000.0); |
| } |
| |
| // These only establish that the clock never runs backwards. Sampling in a loop cannot show that |
| // it runs forwards at all, because performance.now() reports whole and fractional milliseconds |
| // and a thousand calls can finish inside one of them; PerformanceNowAdvances covers that. |
| TEST_F(PolyfillsTest, PerformanceNowIsMonotonic) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(ToBool(RunScript(R"((function() { |
| let last = performance.now(); |
| for (let i = 0; i < 1000; ++i) { |
| const next = performance.now(); |
| if (next < last) { |
| return false; |
| } |
| last = next; |
| } |
| return true; |
| })())"))); |
| } |
| |
| TEST_F(PolyfillsTest, PerformanceNowAdvances) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| double before = ToDouble(RunScript("performance.now()")); |
| std::this_thread::sleep_for(kSleepDuration); |
| double elapsed_ms = ToDouble(RunScript("performance.now()")) - before; |
| |
| // sleep_for() blocks for at least the duration asked of it, so the clock really has moved. |
| // Bounding the reading from above as well as below also pins the unit: a clock reporting |
| // seconds would read 0.05 here, and one reporting microseconds 50000. |
| EXPECT_GE(elapsed_ms, kMinElapsedMs); |
| EXPECT_LT(elapsed_ms, kMaxElapsedMs); |
| } |
| |
| TEST_F(PolyfillsTest, HrtimeBigintIsMonotonic) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_EQ(ToString(RunScript("typeof process.hrtime.bigint()")), "bigint"); |
| |
| EXPECT_TRUE(ToBool(RunScript(R"((function() { |
| let last = process.hrtime.bigint(); |
| for (let i = 0; i < 1000; ++i) { |
| const next = process.hrtime.bigint(); |
| if (next < last) { |
| return false; |
| } |
| last = next; |
| } |
| return true; |
| })())"))); |
| } |
| |
| TEST_F(PolyfillsTest, HrtimeBigintAdvances) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript("globalThis.hrtimeStart = process.hrtime.bigint();"); |
| std::this_thread::sleep_for(kSleepDuration); |
| |
| // The difference is taken in BigInt and only then converted, so no precision is lost before |
| // the division. As above, the upper bound is what pins the unit to nanoseconds. |
| double elapsed_ms = |
| ToDouble(RunScript("Number(process.hrtime.bigint() - globalThis.hrtimeStart) / 1e6")); |
| EXPECT_GE(elapsed_ms, kMinElapsedMs); |
| EXPECT_LT(elapsed_ms, kMaxElapsedMs); |
| } |
| |
| TEST_F(PolyfillsTest, SetImmediateQueuesTheCallbackOnTheLoop) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._immediateRan = false; |
| setImmediate(() => { |
| globalThis._immediateRan = true; |
| }); |
| )"); |
| |
| EXPECT_FALSE(ToBool(RunScript("globalThis._immediateRan"))); |
| |
| loop().RunOneIteration(); |
| |
| EXPECT_TRUE(ToBool(RunScript("globalThis._immediateRan"))); |
| } |
| |
| // Arguments after the callback are forwarded to it, as Node and the web platform do. |
| TEST_F(PolyfillsTest, SetImmediateForwardsExtraArgumentsToTheCallback) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._immediateArgs = null; |
| setImmediate((a, b) => { |
| globalThis._immediateArgs = [a, b]; |
| }, 'x', 42); |
| )"); |
| |
| loop().RunOneIteration(); |
| |
| EXPECT_EQ(ToString(RunScript("globalThis._immediateArgs.join(',')")), "x,42"); |
| } |
| |
| TEST_F(PolyfillsTest, SetTimeoutQueuesTheCallbackAndClearTimeoutWithdrawsIt) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._clearedRan = false; |
| globalThis._timeoutRan = false; |
| const handle = setTimeout(() => { |
| globalThis._clearedRan = true; |
| }, 0); |
| clearTimeout(handle); |
| setTimeout(() => { |
| globalThis._timeoutRan = true; |
| }, 0); |
| )"); |
| |
| loop().RunOneIteration(); |
| |
| EXPECT_FALSE(ToBool(RunScript("globalThis._clearedRan"))); |
| EXPECT_TRUE(ToBool(RunScript("globalThis._timeoutRan"))); |
| } |
| |
| TEST_F(PolyfillsTest, QueueMicrotask) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._microtaskRan = false; |
| queueMicrotask(() => { |
| globalThis._microtaskRan = true; |
| }); |
| )"); |
| |
| EXPECT_FALSE(ToBool(RunScript("globalThis._microtaskRan"))); |
| |
| RunMicrotasks(); |
| |
| EXPECT_TRUE(ToBool(RunScript("globalThis._microtaskRan"))); |
| } |
| |
| TEST_F(PolyfillsTest, TimerGlobals) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE( |
| ToBool(RunScript("typeof setImmediate === 'function' && " |
| "typeof setTimeout === 'function' && " |
| "typeof clearTimeout === 'function' && " |
| "typeof queueMicrotask === 'function'"))); |
| } |
| |
| // Arguments after the delay are forwarded to the callback, as Node and the web platform do. |
| TEST_F(PolyfillsTest, SetTimeoutForwardsExtraArgumentsToTheCallback) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._timeoutArgs = null; |
| setTimeout((a, b) => { |
| globalThis._timeoutArgs = [a, b]; |
| }, 0, 'x', 42); |
| )"); |
| |
| loop().RunOneIteration(); |
| |
| EXPECT_EQ(ToString(RunScript("globalThis._timeoutArgs.join(',')")), "x,42"); |
| } |
| |
| // Negative, NaN, non-numeric and missing delays are all treated as zero, matching the web |
| // platform's clamping rules, so all four of these come due in the very first iteration. |
| TEST_F(PolyfillsTest, ADelayThatIsNotAPositiveNumberIsTreatedAsZero) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| RunScript(R"( |
| globalThis._runs = 0; |
| const count = () => { globalThis._runs++; }; |
| setTimeout(count, -50); |
| setTimeout(count, NaN); |
| setTimeout(count, 'not a number'); |
| setTimeout(count); |
| )"); |
| |
| loop().RunOneIteration(); |
| |
| EXPECT_DOUBLE_EQ(ToDouble(RunScript("globalThis._runs")), 4.0); |
| } |
| |
| // The delay reaches the loop as a duration rather than being dropped or rounded to zero. The loop |
| // reads the clock inside setTimeout(), between `before` and `after`, so `due` is bounded on both |
| // sides by `before + 10s <= due <= after + 10s` regardless of how long RunScript() takes. |
| TEST_F(PolyfillsTest, TheDelayIsPassedThroughToTheLoop) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| const auto before = dawn::node::standalone::EventLoop::Clock::now(); |
| RunScript("setTimeout(() => {}, 10000)"); |
| const auto after = dawn::node::standalone::EventLoop::Clock::now(); |
| |
| const std::optional<dawn::node::standalone::EventLoop::TimePoint> due = loop().NextDueTime(); |
| ASSERT_TRUE(due.has_value()); |
| EXPECT_GE(*due, before + std::chrono::seconds(10)); |
| EXPECT_LE(*due, after + std::chrono::seconds(10)); |
| } |
| |
| TEST_F(PolyfillsTest, TimersRejectCallbacksThatAreNotFunctions) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(Throws("setImmediate()")); |
| EXPECT_TRUE(Throws("setImmediate(42)")); |
| EXPECT_TRUE(Throws("setTimeout()")); |
| EXPECT_TRUE(Throws("setTimeout('globalThis.x = 1', 10)")); |
| } |
| |
| // Clearing a handle that was never issued, or one that has already fired, is defined to do nothing |
| // rather than to fail. |
| TEST_F(PolyfillsTest, ClearingAnUnknownTimerIsNotAnError) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_FALSE(Throws("clearTimeout()")); |
| EXPECT_FALSE(Throws("clearTimeout(undefined)")); |
| EXPECT_FALSE(Throws("clearTimeout('not a handle')")); |
| EXPECT_FALSE(Throws("clearTimeout(9999)")); |
| |
| // Clearing twice, and clearing after the timer has already fired, are both no-ops. |
| RunScript(R"( |
| globalThis._ran = false; |
| globalThis._handle = setTimeout(() => { |
| globalThis._ran = true; |
| }, 0); |
| )"); |
| loop().RunOneIteration(); |
| EXPECT_TRUE(ToBool(RunScript("globalThis._ran"))); |
| |
| EXPECT_FALSE(Throws("clearTimeout(globalThis._handle)")); |
| EXPECT_FALSE(Throws("clearTimeout(globalThis._handle)")); |
| } |
| |
| // Handles have to be distinct, or clearing one timer would cancel another. |
| TEST_F(PolyfillsTest, TimerHandlesAreUnique) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(ToBool(RunScript(R"( |
| const noop = () => {}; |
| const handles = [setTimeout(noop, 1), setTimeout(noop, 1), setTimeout(noop, 1)]; |
| new Set(handles).size === handles.length; |
| )"))); |
| } |
| |
| // Node hands back a Timeout object here. The CTS only ever passes the value back to clearTimeout(), |
| // so a number is enough. |
| TEST_F(PolyfillsTest, SetTimeoutReturnsANumericHandle) { |
| dawn::node::standalone::RegisterPolyfills(env_, loop()); |
| |
| EXPECT_TRUE(ToBool(RunScript("typeof setTimeout(() => {}, 0) === 'number'"))); |
| } |
| |
| } // namespace |
| |
| #pragma clang diagnostic pop |