diff options
author | 2023-05-31 23:12:04 -0700 | |
---|---|---|
committer | 2023-05-31 23:12:04 -0700 | |
commit | e632941c520e9346fc706bb12d0434974c3f5a98 (patch) | |
tree | d80b1895cd920d45d0e74bff11ca90fc4ff2dfcd | |
parent | 176fade220ccc254e5ad822c3bd211023e961074 (diff) | |
download | bun-e632941c520e9346fc706bb12d0434974c3f5a98.tar.gz bun-e632941c520e9346fc706bb12d0434974c3f5a98.tar.zst bun-e632941c520e9346fc706bb12d0434974c3f5a98.zip |
Small improvements to `bun test` (#3071)
* Change status icon for skipped tests from "-" to "»"
* Show file path instead of filename in `bun test`
* Emit collapsable logs when running `bun test` in Github Actions
https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines
* Add fallback for test icons when emojis are not available
* Only check for GITHUB_ACTIONS when running `bun test`
* Emit error annotations when running `bun test` in Github Actions
https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
* Remove ANSI output from Github annotation, it doesn't work
* Remove outdated code from internal test runner
* Add GithubActionFormatter to handle cases where error name or message is already ANSI
* Fix formatting of test
* Fix #3070
* Implement `bun test --run-todo`
By default, `test.todo()` is no longer run, unless `--run-todo` is specified.
* Fix test that relies on test.todo() being run
* Support vitest-style test options
* Disable GITHUB_ACTION in test harness
* Add types for TestOptions
* Fix bug where test.skip() actually ran
* Implement `test.skipIf()` and `describe.skipIf()`
* Implement `test.runIf()`
* Move DiffFormatter to its own file
* Fix bug where Bun.inspect() would emit a Github annotation
* Introduce `bun test --only`, rename `--run-todo` to `--todo`
* Implement `test.if()`, `describe.if()`, and other test fixes
* Remove unwanted files from last commit
* Fix last reference to --run-todo
* Fix memory issues with printing github actions text
* Update bindings.zig
* Fix bug with `test.only()`
* Remove debug test
* Make the github annotations better
* Improve .vscode/launch.json
* Implement `expect().toBeNil()`
* Remove .only() from test
* Implement toBeBoolean(), toBeTrue(), toBeFalse()
* Add lots of matchers
* toBeNil()
* toBeBoolean()
* toBeTrue()
* toBeFalse()
* toBeNumber()
* toBeInteger()
* toBeFinite()
* toBePositive()
* toBeNegative()
* toBeWithin()
* toBeSymbol()
* toBeFunction()
* toBeDate()
* toBeString()
* toInclude()
* toStartWith()
* toEndWith()
* Fix #3135
* Reduce verbosity of test
* Fix snapshot bug
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
25 files changed, 3360 insertions, 700 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index 84b1f6557..1cbd4a79f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,171 +1,189 @@ { + // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits: + // { "initCommands": ["process handle -p false -s false -n false SIGHUP"] } "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", - "name": "bun test", + "name": "bun test [file]", "program": "bun-debug", "args": ["test", "${file}"], - "cwd": "${file}/../", + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + "BUN_DEBUG_QUIET_LOGS": "1" }, - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (fast)", + "name": "bun test [file] (gc)", "program": "bun-debug", "args": ["test", "${file}"], - "cwd": "${file}/../", + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "3" }, - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test --watch", + "name": "bun test [file] (verbose)", "program": "bun-debug", - "args": ["--watch", "test", "${file}"], - "cwd": "${workspaceFolder}", + "args": ["test", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (all)", + "name": "bun test [file] --watch", "program": "bun-debug", - "cwd": "${workspaceFolder}", + "args": ["test", "--watch", "${file}"], + "cwd": "${fileDirname}", "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --only", + "program": "bun-debug", + "args": ["test", "--only", "${file}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" + }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (all, fast)", + "name": "bun test [*]", "program": "bun-debug", "args": ["test"], - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}/test", "env": { + "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run current file", + "name": "bun test [*] (gc)", "program": "bun-debug", - "args": ["${file}"], - "cwd": "${file}/../../", + "args": ["test"], + "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "3" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run (watch)", + "name": "bun test [*] --only", "program": "bun-debug", - "args": ["--watch", "${file}"], - "cwd": "${file}/../../", + "args": ["test", "--only"], + "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run (hot)", + "name": "bun run [file]", "program": "bun-debug", - "args": ["--hot", "${file}"], - "cwd": "${file}/../../", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run", + "name": "bun run [file] (gc)", "program": "bun-debug", - "args": ["check.tsx", "-c"], - "cwd": "${env:HOME}/Build/react-ssr", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "3" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, - { "type": "lldb", "request": "launch", - "name": "bun http example", + "name": "bun run [file] (verbose)", "program": "bun-debug", - "args": ["run", "examples/http.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun http file example", + "name": "bun run [file] --watch", "program": "bun-debug", - "args": ["run", "examples/bun/http-file.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "--watch", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun html-rewriter example", + "name": "bun run [file] --hot", "program": "bun-debug", - "args": ["run", "examples/bun/html-rewriter.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "--hot", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, - { "type": "lldb", "request": "launch", diff --git a/packages/bun-internal-test/runners/bun/runner.ts b/packages/bun-internal-test/runners/bun/runner.ts index 3fb2660be..43bf4586f 100644 --- a/packages/bun-internal-test/runners/bun/runner.ts +++ b/packages/bun-internal-test/runners/bun/runner.ts @@ -318,7 +318,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse }; }; const parseTestLine = (line: string): Test | undefined => { - const match = /^(✓|‚úì|✗|‚úó|-|✎) (.*)$/.exec(line); + const match = /^(✓|‚úì|✗|‚úó|»|-|✎) (.*)$/.exec(line); if (!match) { return undefined; } @@ -333,6 +333,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse case "‚úó": status = "fail"; break; + case "»": case "-": status = "skip"; break; diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index f540b424f..4ded315ff 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -23,30 +23,6 @@ function* findTests(dir, query) { } } -function dump(buf) { - var offset = 0, - length = buf.byteLength; - while (offset < length) { - try { - const wrote = writeSync(1, buf); - offset += wrote; - if (offset < length) { - try { - fsyncSync(1); - } catch (e) {} - - buf = buf.slice(wrote); - } - } catch (e) { - if (e.code === "EAGAIN") { - continue; - } - - throw e; - } - } -} - var failingTests = []; async function runTest(path) { @@ -58,7 +34,7 @@ async function runTest(path) { status: exitCode, error: timedOut, } = spawnSync("bun", ["test", path], { - stdio: ["ignore", "pipe", "pipe"], + stdio: "inherit", timeout: 1000 * 60 * 3, env: { ...process.env, @@ -75,64 +51,8 @@ async function runTest(path) { failingTests.push(name); if (timedOut) console.error(timedOut); } - - if (isAction && !passed) { - findErrors(stdout); - findErrors(stderr); - } - - if (isAction) { - const prefix = passed ? "PASS" : `FAIL`; - action.startGroup(`${prefix} - ${name}`); - } - - stdout && stdout?.byteLength && dump(stdout); - stderr && stderr?.byteLength && dump(stderr); - - if (isAction) { - action.endGroup(); - } } -function findErrors(data) { - const text = new StringDecoder().write(new Buffer(data.buffer)).replaceAll(/\u001b\[.*?m/g, ""); - let index = 0; - do { - index = text.indexOf("error: ", index); - if (index === -1) { - break; - } - - const messageEnd = text.indexOf("\n", index); - if (messageEnd === -1) { - break; - } - const message = text.slice(index + 7, messageEnd); - index = text.indexOf("at ", index); - if (index === -1) { - break; - } - const startAt = index; - index = text.indexOf("\n", index); - if (index === -1) { - break; - } - const at = text.slice(startAt + 3, index); - let file = at.slice(0, at.indexOf(":")); - if (file.length === 0) { - continue; - } - - const startLine = at.slice(at.indexOf(":") + 1, at.indexOf(":") + 1 + at.slice(at.indexOf(":") + 1).indexOf(":")); - const startColumn = at.slice(at.indexOf(":") + 1 + at.slice(at.indexOf(":") + 1).indexOf(":") + 1); - - if (file.startsWith("/")) { - file = relative(cwd, file); - } - - action.error(message, { file, startLine, startColumn }); - } while (index !== -1); -} var tests = []; var testFileNames = []; for (const path of findTests(resolve(cwd, "test"))) { diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 425832c7f..a5cee34c3 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -33,7 +33,41 @@ declare module "bun:test" { */ export type Describe = { (label: string, fn: () => void): void; - skip: (label: string, fn: () => void) => void; + /** + * Skips all other tests, except this group of tests. + * + * @param label the label for the tests + * @param fn the function that defines the tests + */ + only(label: string, fn: () => void): void; + /** + * Skips this group of tests. + * + * @param label the label for the tests + * @param fn the function that defines the tests + */ + skip(label: string, fn: () => void): void; + /** + * Marks this group of tests as to be written or to be fixed. + * + * @param label the label for the tests + * @param fn the function that defines the tests + */ + todo(label: string, fn?: () => void): void; + /** + * Runs this group of tests, only if `condition` is true. + * + * This is the opposite of `describe.skipIf()`. + * + * @param condition if these tests should run + */ + if(condition: boolean): (label: string, fn: () => void) => void; + /** + * Skips this group of tests, if `condition` is true. + * + * @param condition if these tests should be skipped + */ + skipIf(condition: boolean): (label: string, fn: () => void) => void; }; /** * Describes a group of related tests. @@ -122,6 +156,31 @@ declare module "bun:test" { | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), ): void; + export type TestOptions = { + /** + * Sets the timeout for the test in milliseconds. + * + * If the test does not complete within this time, the test will fail with: + * ```ts + * 'Timeout: test {name} timed out after 5000ms' + * ``` + * + * @default 5000 // 5 seconds + */ + timeout?: number; + /** + * Sets the number of times to retry the test if it fails. + * + * @default 0 + */ + retry?: number; + /** + * Sets the number of times to repeat the test, regardless of whether it passed or failed. + * + * @default 0 + */ + repeats?: number; + }; /** * Runs a test. * @@ -135,8 +194,13 @@ declare module "bun:test" { * expect(response.ok).toBe(true); * }); * + * test("can set a timeout", async () => { + * await Bun.sleep(100); + * }, 50); // or { timeout: 50 } + * * @param label the label for the test * @param fn the test function + * @param options the test timeout or options */ export type Test = { ( @@ -145,45 +209,44 @@ declare module "bun:test" { | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), /** - * @default 300_000 milliseconds (5 minutes) - * - * After this many milliseconds, the test will fail with an error message like: - * ```ts - * 'Timeout: test "name" timed out after 300_000ms' - * ``` + * - If a `number`, sets the timeout for the test in milliseconds. + * - If an `object`, sets the options for the test. + * - `timeout` sets the timeout for the test in milliseconds. + * - `retry` sets the number of times to retry the test if it fails. + * - `repeats` sets the number of times to repeat the test, regardless of whether it passed or failed. */ - timeoutMs?: number, + options?: number | TestOptions, ): void; /** * Skips all other tests, except this test. - * @deprecated Not yet implemented. * * @param label the label for the test * @param fn the test function - * @param timeoutMs the timeout for the test + * @param options the test timeout or options */ only( label: string, fn: | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), - timeoutMs?: number, + options?: number | TestOptions, ): void; /** * Skips this test. * * @param label the label for the test * @param fn the test function + * @param options the test timeout or options */ skip( label: string, fn: | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), - timeoutMs?: number, + options?: number | TestOptions, ): void; /** - * Indicate a test is yet to be written or implemented correctly. + * Marks this test as to be written or to be fixed. * * When a test function is passed, it will be marked as `todo` in the test results * as long the test does not pass. When the test passes, the test will be marked as @@ -192,13 +255,45 @@ declare module "bun:test" { * * @param label the label for the test * @param fn the test function + * @param options the test timeout or options */ todo( label: string, fn?: | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, ): void; + /** + * Runs this test, if `condition` is true. + * + * This is the opposite of `test.skipIf()`. + * + * @param condition if the test should run + */ + if( + condition: boolean, + ): ( + label: string, + fn: + | (() => void | Promise<unknown>) + | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, + ) => void; + /** + * Skips this test, if `condition` is true. + * + * @param condition if the test should be skipped + */ + skipIf( + condition: boolean, + ): ( + label: string, + fn: + | (() => void | Promise<unknown>) + | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, + ) => void; }; /** * Runs a test. @@ -533,6 +628,160 @@ declare module "bun:test" { * expect(new Set()).toBeEmpty(); */ toBeEmpty(): void; + /** + * Asserts that a value is `null` or `undefined`. + * + * @example + * expect(null).toBeNil(); + * expect(undefined).toBeNil(); + */ + toBeNil(): void; + /** + * Asserts that a value is a `boolean`. + * + * @example + * expect(true).toBeBoolean(); + * expect(false).toBeBoolean(); + * expect(null).not.toBeBoolean(); + * expect(0).not.toBeBoolean(); + */ + toBeBoolean(): void; + /** + * Asserts that a value is `true`. + * + * @example + * expect(true).toBeTrue(); + * expect(false).not.toBeTrue(); + * expect(1).not.toBeTrue(); + */ + toBeTrue(): void; + /** + * Asserts that a value is `false`. + * + * @example + * expect(false).toBeFalse(); + * expect(true).not.toBeFalse(); + * expect(0).not.toBeFalse(); + */ + toBeFalse(): void; + /** + * Asserts that a value is a `number`. + * + * @example + * expect(1).toBeNumber(); + * expect(3.14).toBeNumber(); + * expect(NaN).toBeNumber(); + * expect(BigInt(1)).not.toBeNumber(); + */ + toBeNumber(): void; + /** + * Asserts that a value is a `number`, and is an integer. + * + * @example + * expect(1).toBeInteger(); + * expect(3.14).not.toBeInteger(); + * expect(NaN).not.toBeInteger(); + */ + toBeInteger(): void; + /** + * Asserts that a value is a `number`, and is not `NaN` or `Infinity`. + * + * @example + * expect(1).toBeFinite(); + * expect(3.14).toBeFinite(); + * expect(NaN).not.toBeFinite(); + * expect(Infinity).not.toBeFinite(); + */ + toBeFinite(): void; + /** + * Asserts that a value is a positive `number`. + * + * @example + * expect(1).toBePositive(); + * expect(-3.14).not.toBePositive(); + * expect(NaN).not.toBePositive(); + */ + toBePositive(): void; + /** + * Asserts that a value is a negative `number`. + * + * @example + * expect(-3.14).toBeNegative(); + * expect(1).not.toBeNegative(); + * expect(NaN).not.toBeNegative(); + */ + toBeNegative(): void; + /** + * Asserts that a value is a number between a start and end value. + * + * @param start the start number (inclusive) + * @param end the end number (exclusive) + */ + toBeWithin(start: number, end: number): void; + /** + * Asserts that a value is a `symbol`. + * + * @example + * expect(Symbol("foo")).toBeSymbol(); + * expect("foo").not.toBeSymbol(); + */ + toBeSymbol(): void; + /** + * Asserts that a value is a `function`. + * + * @example + * expect(() => {}).toBeFunction(); + */ + toBeFunction(): void; + /** + * Asserts that a value is a `Date` object. + * + * To check if a date is valid, use `toBeValidDate()` instead. + * + * @example + * expect(new Date()).toBeDate(); + * expect(new Date(null)).toBeDate(); + * expect("2020-03-01").not.toBeDate(); + */ + toBeDate(): void; + /** + * Asserts that a value is a valid `Date` object. + * + * @example + * expect(new Date()).toBeValidDate(); + * expect(new Date(null)).not.toBeValidDate(); + * expect("2020-03-01").not.toBeValidDate(); + */ + toBeValidDate(): void; + /** + * Asserts that a value is a `string`. + * + * @example + * expect("foo").toBeString(); + * expect(new String("bar")).toBeString(); + * expect(123).not.toBeString(); + */ + toBeString(): void; + /** + * Asserts that a value includes a `string`. + * + * For non-string values, use `toContain()` instead. + * + * @param expected the expected substring + */ + toInclude(expected: string): void; + /** + * Asserts that a value starts with a `string`. + * + * @param expected the string to start with + */ + toStartWith(expected: string): void; + /** + * Asserts that a value ends with a `string`. + * + * @param expected the string to end with + */ + toEndWith(expected: string): void; }; } diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 5482c461f..86f0ab29d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -2665,9 +2665,15 @@ JSC_DECLARE_CUSTOM_GETTER(ExpectPrototype__resolvesGetterWrap); extern "C" EncodedJSValue ExpectPrototype__toBe(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeBoolean(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeBooleanCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeCloseTo(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeDate(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeDateCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeDefined(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback); @@ -2677,9 +2683,18 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEmptyCallback); extern "C" EncodedJSValue ExpectPrototype__toBeEven(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeFalse(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFalseCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeFalsy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeFinite(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFiniteCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeFunction(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFunctionCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeGreaterThan(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanCallback); @@ -2689,6 +2704,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanOrEqualCallback); extern "C" EncodedJSValue ExpectPrototype__toBeInstanceOf(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeInstanceOfCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeInteger(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeIntegerCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeLessThan(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeLessThanCallback); @@ -2698,24 +2716,51 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeLessThanOrEqualCallback); extern "C" EncodedJSValue ExpectPrototype__toBeNaN(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNaNCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeNegative(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNegativeCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeNil(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNilCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeNull(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeNumber(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNumberCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeOdd(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback); +extern "C" EncodedJSValue ExpectPrototype__toBePositive(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBePositiveCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeString(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeStringCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeSymbol(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeSymbolCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeTrue(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeTrueCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeTruthy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeTruthyCallback); extern "C" EncodedJSValue ExpectPrototype__toBeUndefined(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeUndefinedCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeWithin(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeWithinCallback); + extern "C" EncodedJSValue ExpectPrototype__toContain(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toContainCallback); extern "C" EncodedJSValue ExpectPrototype__toContainEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toContainEqualCallback); +extern "C" EncodedJSValue ExpectPrototype__toEndWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback); + extern "C" EncodedJSValue ExpectPrototype__toEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEqualCallback); @@ -2749,6 +2794,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedTimesCallback); extern "C" EncodedJSValue ExpectPrototype__toHaveReturnedWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedWithCallback); +extern "C" EncodedJSValue ExpectPrototype__toInclude(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback); + extern "C" EncodedJSValue ExpectPrototype__toMatch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchCallback); @@ -2761,6 +2809,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchObjectCallback); extern "C" EncodedJSValue ExpectPrototype__toMatchSnapshot(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback); +extern "C" EncodedJSValue ExpectPrototype__toStartWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback); + extern "C" EncodedJSValue ExpectPrototype__toStrictEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toStrictEqualCallback); @@ -2780,23 +2831,38 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "rejects"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__rejectsGetterWrap, 0 } }, { "resolves"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__resolvesGetterWrap, 0 } }, { "toBe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCallback, 1 } }, + { "toBeBoolean"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeBooleanCallback, 0 } }, { "toBeCloseTo"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCloseToCallback, 1 } }, + { "toBeDate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeDateCallback, 0 } }, { "toBeDefined"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeDefinedCallback, 0 } }, { "toBeEmpty"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEmptyCallback, 0 } }, { "toBeEven"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEvenCallback, 0 } }, + { "toBeFalse"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFalseCallback, 0 } }, { "toBeFalsy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFalsyCallback, 0 } }, + { "toBeFinite"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFiniteCallback, 0 } }, + { "toBeFunction"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFunctionCallback, 0 } }, { "toBeGreaterThan"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeGreaterThanCallback, 1 } }, { "toBeGreaterThanOrEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeGreaterThanOrEqualCallback, 1 } }, { "toBeInstanceOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeInstanceOfCallback, 1 } }, + { "toBeInteger"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeIntegerCallback, 0 } }, { "toBeLessThan"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeLessThanCallback, 1 } }, { "toBeLessThanOrEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeLessThanOrEqualCallback, 1 } }, { "toBeNaN"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNaNCallback, 0 } }, + { "toBeNegative"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNegativeCallback, 0 } }, + { "toBeNil"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNilCallback, 0 } }, { "toBeNull"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNullCallback, 0 } }, + { "toBeNumber"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNumberCallback, 0 } }, { "toBeOdd"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeOddCallback, 0 } }, + { "toBePositive"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBePositiveCallback, 0 } }, + { "toBeString"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeStringCallback, 0 } }, + { "toBeSymbol"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeSymbolCallback, 0 } }, + { "toBeTrue"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeTrueCallback, 0 } }, { "toBeTruthy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeTruthyCallback, 0 } }, { "toBeUndefined"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeUndefinedCallback, 0 } }, + { "toBeWithin"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeWithinCallback, 2 } }, { "toContain"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainCallback, 1 } }, { "toContainEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainEqualCallback, 1 } }, + { "toEndWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEndWithCallback, 1 } }, { "toEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEqualCallback, 1 } }, { "toHaveBeenCalledTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledTimesCallback, 1 } }, { "toHaveBeenCalledWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledWithCallback, 1 } }, @@ -2808,10 +2874,12 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "toHaveProperty"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHavePropertyCallback, 2 } }, { "toHaveReturnedTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedTimesCallback, 1 } }, { "toHaveReturnedWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedWithCallback, 1 } }, + { "toInclude"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toIncludeCallback, 1 } }, { "toMatch"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchCallback, 1 } }, { "toMatchInlineSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchInlineSnapshotCallback, 1 } }, { "toMatchObject"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchObjectCallback, 1 } }, { "toMatchSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchSnapshotCallback, 1 } }, + { "toStartWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStartWithCallback, 1 } }, { "toStrictEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStrictEqualCallback, 1 } }, { "toThrow"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toThrowCallback, 1 } }, { "toThrowErrorMatchingInlineSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toThrowErrorMatchingInlineSnapshotCallback, 1 } }, @@ -2895,6 +2963,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCallback, (JSGlobalObject * lexica return ExpectPrototype__toBe(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeBooleanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeBoolean(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -2922,6 +3017,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback, (JSGlobalObject * return ExpectPrototype__toBeCloseTo(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeDateCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeDate(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3003,6 +3125,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback, (JSGlobalObject * le return ExpectPrototype__toBeEven(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalseCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFalse(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3030,6 +3179,60 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback, (JSGlobalObject * l return ExpectPrototype__toBeFalsy(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFiniteCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFinite(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFunctionCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFunction(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3111,6 +3314,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeInstanceOfCallback, (JSGlobalObjec return ExpectPrototype__toBeInstanceOf(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeIntegerCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeInteger(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeLessThanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3192,6 +3422,60 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNaNCallback, (JSGlobalObject * lex return ExpectPrototype__toBeNaN(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNegativeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNegative(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNilCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNil(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3219,6 +3503,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback, (JSGlobalObject * le return ExpectPrototype__toBeNull(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNumberCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNumber(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3246,6 +3557,114 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback, (JSGlobalObject * lex return ExpectPrototype__toBeOdd(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBePositiveCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBePositive(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeStringCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeString(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeSymbolCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeSymbol(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeTrueCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeTrue(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeTruthyCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3300,6 +3719,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeUndefinedCallback, (JSGlobalObject return ExpectPrototype__toBeUndefined(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeWithinCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeWithin(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toContainCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3354,6 +3800,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toContainEqualCallback, (JSGlobalObjec return ExpectPrototype__toContainEqual(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toEndWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEqualCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3651,6 +4124,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedWithCallback, (JSGlobalO return ExpectPrototype__toHaveReturnedWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toInclude(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3759,6 +4259,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback, (JSGlobalObje return ExpectPrototype__toMatchSnapshot(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toStartWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toStrictEqualCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2b569282e..3c100d66c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -160,6 +160,22 @@ pub const ZigString = extern struct { }; } + pub fn indexOfAny(this: ZigString, comptime chars: []const u8) ?strings.OptionalUsize { + if (this.is16Bit()) { + return strings.indexOfAny16(this.utf16SliceAligned(), chars); + } else { + return strings.indexOfAny(this.slice(), chars); + } + } + + pub fn charAt(this: ZigString, offset: usize) u8 { + if (this.is16Bit()) { + return @truncate(u8, this.utf16SliceAligned()[offset]); + } else { + return @truncate(u8, this.slice()[offset]); + } + } + pub fn eql(this: ZigString, other: ZigString) bool { if (this.len == 0 or other.len == 0) return this.len == other.len; @@ -225,14 +241,7 @@ pub const ZigString = extern struct { return this.slice()[0] == char; } - pub fn substring(this: ZigString, offset: usize, maxlen: usize) ZigString { - var len: usize = undefined; - if (maxlen == 0) { - len = this.len; - } else { - len = @max(this.len, maxlen); - } - + pub fn substringWithLen(this: ZigString, offset: usize, len: usize) ZigString { if (this.is16Bit()) { return ZigString.from16Slice(this.utf16SliceAligned()[@min(this.len, offset)..len]); } @@ -249,6 +258,17 @@ pub const ZigString = extern struct { return out; } + pub fn substring(this: ZigString, offset: usize, maxlen: usize) ZigString { + var len: usize = undefined; + if (maxlen == 0) { + len = this.len; + } else { + len = @max(this.len, maxlen); + } + + return this.substringWithLen(offset, len); + } + pub fn maxUTF8ByteLength(this: ZigString) usize { if (this.isUTF8()) return this.len; @@ -504,6 +524,20 @@ pub const ZigString = extern struct { return &Holder.value; } + pub const GithubActionFormatter = struct { + text: ZigString, + + pub fn format(this: GithubActionFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + var bytes = this.text.toSlice(bun.default_allocator); + defer bytes.deinit(); + try strings.githubActionWriter(writer, bytes.slice()); + } + }; + + pub fn githubAction(this: ZigString) GithubActionFormatter { + return GithubActionFormatter{ .text = this }; + } + pub fn toAtomicValue(this: *const ZigString, globalThis: *JSC.JSGlobalObject) JSValue { return shim.cppFn("toAtomicValue", .{ this, globalThis }); } @@ -3406,6 +3440,10 @@ pub const JSValue = enum(JSValueReprInt) { return this.jsType() == .RegExpObject; } + pub fn isDate(this: JSValue) bool { + return this.jsType() == .JSDate; + } + pub fn asCheckLoaded(value: JSValue, comptime ZigType: type) ?*ZigType { if (!ZigType.Class.isLoaded() or value.isUndefinedOrNull()) return null; diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index b993701fc..cd63e4fe9 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -1932,7 +1932,7 @@ pub const ZigConsoleClient = struct { if (str.is16Bit()) { // streaming print - writer.print("{s}", .{str}); + writer.print("{}", .{str}); } else if (strings.isAllASCII(str.slice())) { // fast path writer.writeAll(str.slice()); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index a30774aa1..d24c8c81f 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -878,40 +878,70 @@ pub const JSExpect = struct { @compileLog("Expected Expect.getResolves to be a getter with thisValue"); if (@TypeOf(Expect.toBe) != CallbackType) @compileLog("Expected Expect.toBe to be a callback but received " ++ @typeName(@TypeOf(Expect.toBe))); + if (@TypeOf(Expect.toBeBoolean) != CallbackType) + @compileLog("Expected Expect.toBeBoolean to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeBoolean))); if (@TypeOf(Expect.toBeCloseTo) != CallbackType) @compileLog("Expected Expect.toBeCloseTo to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeCloseTo))); + if (@TypeOf(Expect.toBeDate) != CallbackType) + @compileLog("Expected Expect.toBeDate to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeDate))); if (@TypeOf(Expect.toBeDefined) != CallbackType) @compileLog("Expected Expect.toBeDefined to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeDefined))); if (@TypeOf(Expect.toBeEmpty) != CallbackType) @compileLog("Expected Expect.toBeEmpty to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEmpty))); if (@TypeOf(Expect.toBeEven) != CallbackType) @compileLog("Expected Expect.toBeEven to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEven))); + if (@TypeOf(Expect.toBeFalse) != CallbackType) + @compileLog("Expected Expect.toBeFalse to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFalse))); if (@TypeOf(Expect.toBeFalsy) != CallbackType) @compileLog("Expected Expect.toBeFalsy to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFalsy))); + if (@TypeOf(Expect.toBeFinite) != CallbackType) + @compileLog("Expected Expect.toBeFinite to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFinite))); + if (@TypeOf(Expect.toBeFunction) != CallbackType) + @compileLog("Expected Expect.toBeFunction to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFunction))); if (@TypeOf(Expect.toBeGreaterThan) != CallbackType) @compileLog("Expected Expect.toBeGreaterThan to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeGreaterThan))); if (@TypeOf(Expect.toBeGreaterThanOrEqual) != CallbackType) @compileLog("Expected Expect.toBeGreaterThanOrEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeGreaterThanOrEqual))); if (@TypeOf(Expect.toBeInstanceOf) != CallbackType) @compileLog("Expected Expect.toBeInstanceOf to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeInstanceOf))); + if (@TypeOf(Expect.toBeInteger) != CallbackType) + @compileLog("Expected Expect.toBeInteger to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeInteger))); if (@TypeOf(Expect.toBeLessThan) != CallbackType) @compileLog("Expected Expect.toBeLessThan to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeLessThan))); if (@TypeOf(Expect.toBeLessThanOrEqual) != CallbackType) @compileLog("Expected Expect.toBeLessThanOrEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeLessThanOrEqual))); if (@TypeOf(Expect.toBeNaN) != CallbackType) @compileLog("Expected Expect.toBeNaN to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNaN))); + if (@TypeOf(Expect.toBeNegative) != CallbackType) + @compileLog("Expected Expect.toBeNegative to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNegative))); + if (@TypeOf(Expect.toBeNil) != CallbackType) + @compileLog("Expected Expect.toBeNil to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNil))); if (@TypeOf(Expect.toBeNull) != CallbackType) @compileLog("Expected Expect.toBeNull to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNull))); + if (@TypeOf(Expect.toBeNumber) != CallbackType) + @compileLog("Expected Expect.toBeNumber to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNumber))); if (@TypeOf(Expect.toBeOdd) != CallbackType) @compileLog("Expected Expect.toBeOdd to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeOdd))); + if (@TypeOf(Expect.toBePositive) != CallbackType) + @compileLog("Expected Expect.toBePositive to be a callback but received " ++ @typeName(@TypeOf(Expect.toBePositive))); + if (@TypeOf(Expect.toBeString) != CallbackType) + @compileLog("Expected Expect.toBeString to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeString))); + if (@TypeOf(Expect.toBeSymbol) != CallbackType) + @compileLog("Expected Expect.toBeSymbol to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeSymbol))); + if (@TypeOf(Expect.toBeTrue) != CallbackType) + @compileLog("Expected Expect.toBeTrue to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeTrue))); if (@TypeOf(Expect.toBeTruthy) != CallbackType) @compileLog("Expected Expect.toBeTruthy to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeTruthy))); if (@TypeOf(Expect.toBeUndefined) != CallbackType) @compileLog("Expected Expect.toBeUndefined to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeUndefined))); + if (@TypeOf(Expect.toBeWithin) != CallbackType) + @compileLog("Expected Expect.toBeWithin to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeWithin))); if (@TypeOf(Expect.toContain) != CallbackType) @compileLog("Expected Expect.toContain to be a callback but received " ++ @typeName(@TypeOf(Expect.toContain))); if (@TypeOf(Expect.toContainEqual) != CallbackType) @compileLog("Expected Expect.toContainEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toContainEqual))); + if (@TypeOf(Expect.toEndWith) != CallbackType) + @compileLog("Expected Expect.toEndWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toEndWith))); if (@TypeOf(Expect.toEqual) != CallbackType) @compileLog("Expected Expect.toEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toEqual))); if (@TypeOf(Expect.toHaveBeenCalledTimes) != CallbackType) @@ -934,6 +964,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toHaveReturnedTimes to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveReturnedTimes))); if (@TypeOf(Expect.toHaveReturnedWith) != CallbackType) @compileLog("Expected Expect.toHaveReturnedWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveReturnedWith))); + if (@TypeOf(Expect.toInclude) != CallbackType) + @compileLog("Expected Expect.toInclude to be a callback but received " ++ @typeName(@TypeOf(Expect.toInclude))); if (@TypeOf(Expect.toMatch) != CallbackType) @compileLog("Expected Expect.toMatch to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatch))); if (@TypeOf(Expect.toMatchInlineSnapshot) != CallbackType) @@ -942,6 +974,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toMatchObject to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchObject))); if (@TypeOf(Expect.toMatchSnapshot) != CallbackType) @compileLog("Expected Expect.toMatchSnapshot to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchSnapshot))); + if (@TypeOf(Expect.toStartWith) != CallbackType) + @compileLog("Expected Expect.toStartWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toStartWith))); if (@TypeOf(Expect.toStrictEqual) != CallbackType) @compileLog("Expected Expect.toStrictEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toStrictEqual))); if (@TypeOf(Expect.toThrow) != CallbackType) @@ -1002,23 +1036,38 @@ pub const JSExpect = struct { @export(Expect.stringContaining, .{ .name = "ExpectClass__stringContaining" }); @export(Expect.stringMatching, .{ .name = "ExpectClass__stringMatching" }); @export(Expect.toBe, .{ .name = "ExpectPrototype__toBe" }); + @export(Expect.toBeBoolean, .{ .name = "ExpectPrototype__toBeBoolean" }); @export(Expect.toBeCloseTo, .{ .name = "ExpectPrototype__toBeCloseTo" }); + @export(Expect.toBeDate, .{ .name = "ExpectPrototype__toBeDate" }); @export(Expect.toBeDefined, .{ .name = "ExpectPrototype__toBeDefined" }); @export(Expect.toBeEmpty, .{ .name = "ExpectPrototype__toBeEmpty" }); @export(Expect.toBeEven, .{ .name = "ExpectPrototype__toBeEven" }); + @export(Expect.toBeFalse, .{ .name = "ExpectPrototype__toBeFalse" }); @export(Expect.toBeFalsy, .{ .name = "ExpectPrototype__toBeFalsy" }); + @export(Expect.toBeFinite, .{ .name = "ExpectPrototype__toBeFinite" }); + @export(Expect.toBeFunction, .{ .name = "ExpectPrototype__toBeFunction" }); @export(Expect.toBeGreaterThan, .{ .name = "ExpectPrototype__toBeGreaterThan" }); @export(Expect.toBeGreaterThanOrEqual, .{ .name = "ExpectPrototype__toBeGreaterThanOrEqual" }); @export(Expect.toBeInstanceOf, .{ .name = "ExpectPrototype__toBeInstanceOf" }); + @export(Expect.toBeInteger, .{ .name = "ExpectPrototype__toBeInteger" }); @export(Expect.toBeLessThan, .{ .name = "ExpectPrototype__toBeLessThan" }); @export(Expect.toBeLessThanOrEqual, .{ .name = "ExpectPrototype__toBeLessThanOrEqual" }); @export(Expect.toBeNaN, .{ .name = "ExpectPrototype__toBeNaN" }); + @export(Expect.toBeNegative, .{ .name = "ExpectPrototype__toBeNegative" }); + @export(Expect.toBeNil, .{ .name = "ExpectPrototype__toBeNil" }); @export(Expect.toBeNull, .{ .name = "ExpectPrototype__toBeNull" }); + @export(Expect.toBeNumber, .{ .name = "ExpectPrototype__toBeNumber" }); @export(Expect.toBeOdd, .{ .name = "ExpectPrototype__toBeOdd" }); + @export(Expect.toBePositive, .{ .name = "ExpectPrototype__toBePositive" }); + @export(Expect.toBeString, .{ .name = "ExpectPrototype__toBeString" }); + @export(Expect.toBeSymbol, .{ .name = "ExpectPrototype__toBeSymbol" }); + @export(Expect.toBeTrue, .{ .name = "ExpectPrototype__toBeTrue" }); @export(Expect.toBeTruthy, .{ .name = "ExpectPrototype__toBeTruthy" }); @export(Expect.toBeUndefined, .{ .name = "ExpectPrototype__toBeUndefined" }); + @export(Expect.toBeWithin, .{ .name = "ExpectPrototype__toBeWithin" }); @export(Expect.toContain, .{ .name = "ExpectPrototype__toContain" }); @export(Expect.toContainEqual, .{ .name = "ExpectPrototype__toContainEqual" }); + @export(Expect.toEndWith, .{ .name = "ExpectPrototype__toEndWith" }); @export(Expect.toEqual, .{ .name = "ExpectPrototype__toEqual" }); @export(Expect.toHaveBeenCalledTimes, .{ .name = "ExpectPrototype__toHaveBeenCalledTimes" }); @export(Expect.toHaveBeenCalledWith, .{ .name = "ExpectPrototype__toHaveBeenCalledWith" }); @@ -1030,10 +1079,12 @@ pub const JSExpect = struct { @export(Expect.toHaveProperty, .{ .name = "ExpectPrototype__toHaveProperty" }); @export(Expect.toHaveReturnedTimes, .{ .name = "ExpectPrototype__toHaveReturnedTimes" }); @export(Expect.toHaveReturnedWith, .{ .name = "ExpectPrototype__toHaveReturnedWith" }); + @export(Expect.toInclude, .{ .name = "ExpectPrototype__toInclude" }); @export(Expect.toMatch, .{ .name = "ExpectPrototype__toMatch" }); @export(Expect.toMatchInlineSnapshot, .{ .name = "ExpectPrototype__toMatchInlineSnapshot" }); @export(Expect.toMatchObject, .{ .name = "ExpectPrototype__toMatchObject" }); @export(Expect.toMatchSnapshot, .{ .name = "ExpectPrototype__toMatchSnapshot" }); + @export(Expect.toStartWith, .{ .name = "ExpectPrototype__toStartWith" }); @export(Expect.toStrictEqual, .{ .name = "ExpectPrototype__toStrictEqual" }); @export(Expect.toThrow, .{ .name = "ExpectPrototype__toThrow" }); @export(Expect.toThrowErrorMatchingInlineSnapshot, .{ .name = "ExpectPrototype__toThrowErrorMatchingInlineSnapshot" }); diff --git a/src/bun.js/builtins/WebCoreJSBuiltins.cpp b/src/bun.js/builtins/WebCoreJSBuiltins.cpp index b6a0863a7..1e270f1ce 100644 --- a/src/bun.js/builtins/WebCoreJSBuiltins.cpp +++ b/src/bun.js/builtins/WebCoreJSBuiltins.cpp @@ -2198,7 +2198,7 @@ const JSC::ConstructorKind s_importMetaObjectRequireESMCodeConstructorKind = JSC const JSC::ImplementationVisibility s_importMetaObjectRequireESMCodeImplementationVisibility = JSC::ImplementationVisibility::Public; const int s_importMetaObjectRequireESMCodeLength = 419; static const JSC::Intrinsic s_importMetaObjectRequireESMCodeIntrinsic = JSC::NoIntrinsic; -const char* const s_importMetaObjectRequireESMCode = "(function (a){\"use strict\";var i=@Loader.registry.@get(a);if(!i||!i.evaluated)i=@loadCJS2ESM(a);if(!i||!i.evaluated||!i.module)@throwTypeError(`require() failed to evaluate module \"${a}\". This is an internal consistentency error.`);var u=@Loader.getModuleNamespaceObject(i.module);if(u[@commonJSSymbol]===0)return;var E=u.default,_=E\?.[@commonJSSymbol];if(_===0)return E;else if(_&&@isCallable(E))return E();return u})\n"; +const char* const s_importMetaObjectRequireESMCode = "(function (i){\"use strict\";var a=@Loader.registry.@get(i);if(!a||!a.evaluated)a=@loadCJS2ESM(i);if(!a||!a.evaluated||!a.module)@throwTypeError(`require() failed to evaluate module \"${i}\". This is an internal consistentency error.`);var E=@Loader.getModuleNamespaceObject(a.module);if(E[@commonJSSymbol]===0)return;var b=E.default,f=b\?.[@commonJSSymbol];if(f===0)return b;else if(f&&@isCallable(b))return b();return E})\n"; // internalRequire const JSC::ConstructAbility s_importMetaObjectInternalRequireCodeConstructAbility = JSC::ConstructAbility::CannotConstruct; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 5c158a4fb..e09b609cb 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -442,6 +442,8 @@ pub const VirtualMachine = struct { onUnhandledRejectionCtx: ?*anyopaque = null, unhandled_error_counter: usize = 0, + on_exception: ?*const OnException = null, + modules: ModuleLoader.AsyncModule.Queue = .{}, aggressive_garbage_collection: GCLevel = GCLevel.none, @@ -449,6 +451,16 @@ pub const VirtualMachine = struct { pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void; + pub const OnException = fn (*ZigException) void; + + pub fn setOnException(this: *VirtualMachine, callback: *const OnException) void { + this.on_exception = callback; + } + + pub fn clearOnException(this: *VirtualMachine) void { + this.on_exception = null; + } + const VMHolder = struct { pub threadlocal var vm: ?*VirtualMachine = null; }; @@ -2068,6 +2080,9 @@ pub const VirtualMachine = struct { var exception = exception_holder.zigException(); this.remapZigException(exception, error_instance, exception_list); this.had_errors = true; + defer if (this.on_exception) |cb| { + cb(exception); + }; var line_numbers = exception.stack.source_lines_numbers[0..exception.stack.source_lines_len]; var max_line: i32 = -1; @@ -2093,6 +2108,7 @@ pub const VirtualMachine = struct { var name = exception.name; const message = exception.message; + var did_print_name = false; if (source_lines.next()) |source| brk: { if (source.text.len == 0) break :brk; @@ -2228,7 +2244,7 @@ pub const VirtualMachine = struct { } if (show.syscall) { - try writer.print(comptime Output.prettyFmt("syscall<d>: <r><cyan>\"{s}\"<r>\n", allow_ansi_color), .{exception.syscall}); + try writer.print(comptime Output.prettyFmt(" syscall<d>: <r><cyan>\"{s}\"<r>\n", allow_ansi_color), .{exception.syscall}); add_extra_line = true; } @@ -2236,7 +2252,7 @@ pub const VirtualMachine = struct { if (show.syscall) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt("errno<d>: <r><yellow>{d}<r>\n", allow_ansi_color), .{exception.errno}); + try writer.print(comptime Output.prettyFmt(" errno<d>: <r><yellow>{d}<r>\n", allow_ansi_color), .{exception.errno}); add_extra_line = true; } diff --git a/src/bun.js/test/diff_format.zig b/src/bun.js/test/diff_format.zig new file mode 100644 index 000000000..4558a5f39 --- /dev/null +++ b/src/bun.js/test/diff_format.zig @@ -0,0 +1,293 @@ +const std = @import("std"); +const bun = @import("root").bun; +const MutableString = bun.MutableString; +const Output = bun.Output; +const default_allocator = bun.default_allocator; +const string = bun.string; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const ZigConsoleClient = JSC.ZigConsoleClient; +const DiffMatchPatch = @import("../../deps/diffz/DiffMatchPatch.zig"); + +pub const DiffFormatter = struct { + received_string: ?string = null, + expected_string: ?string = null, + received: ?JSValue = null, + expected: ?JSValue = null, + globalObject: *JSGlobalObject, + not: bool = false, + + pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (this.expected_string != null and this.received_string != null) { + const received = this.received_string.?; + const expected = this.expected_string.?; + + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received, expected, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + } + + if (this.received == null or this.expected == null) return; + + const received = this.received.?; + const expected = this.expected.?; + var received_buf = MutableString.init(default_allocator, 0) catch unreachable; + var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; + defer { + received_buf.deinit(); + expected_buf.deinit(); + } + + { + var buffered_writer_ = MutableString.BufferedWriter{ .context = &received_buf }; + var buffered_writer = &buffered_writer_; + + var buf_writer = buffered_writer.writer(); + const Writer = @TypeOf(buf_writer); + + const fmt_options = ZigConsoleClient.FormatOptions{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .ordered_properties = true, + .quote_strings = true, + }; + ZigConsoleClient.format( + .Debug, + this.globalObject, + @ptrCast([*]const JSValue, &received), + 1, + Writer, + Writer, + buf_writer, + fmt_options, + ); + buffered_writer.flush() catch unreachable; + + buffered_writer_.context = &expected_buf; + + ZigConsoleClient.format( + .Debug, + this.globalObject, + @ptrCast([*]const JSValue, &this.expected), + 1, + Writer, + Writer, + buf_writer, + fmt_options, + ); + buffered_writer.flush() catch unreachable; + } + + const received_slice = received_buf.toOwnedSliceLeaky(); + const expected_slice = expected_buf.toOwnedSliceLeaky(); + + if (this.not) { + const not_fmt = "Expected: not <green>{s}<r>"; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(not_fmt, true), .{expected_slice}); + } else { + try writer.print(Output.prettyFmt(not_fmt, false), .{expected_slice}); + } + return; + } + + switch (received.determineDiffMethod(expected, this.globalObject)) { + .none => { + const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; + var formatter = ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(fmt, true), .{ + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), + }); + return; + } + + try writer.print(Output.prettyFmt(fmt, true), .{ + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), + }); + return; + }, + .character => { + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received_slice, expected_slice, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + }, + .line => { + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diffLines(default_allocator, received_slice, expected_slice); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d> {s}<r>"; + const delete_fmt = "<red>+ {s}<r>"; + const insert_fmt = "<green>- {s}<r>"; + + var insert_count: usize = 0; + var delete_count: usize = 0; + + for (diffs.items) |df| { + var prev: usize = 0; + var curr: usize = 0; + switch (df.operation) { + .equal => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + .insert => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + insert_count += 1; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + .delete => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + delete_count += 1; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + } + if (df.text[df.text.len - 1] != '\n') try writer.writeAll("\n"); + } + + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt("\n<green>- Expected - {d}<r>\n", true), .{insert_count}); + try writer.print(Output.prettyFmt("<red>+ Received + {d}<r>", true), .{delete_count}); + return; + } + try writer.print("\n- Expected - {d}\n", .{insert_count}); + try writer.print("+ Received + {d}", .{delete_count}); + return; + }, + .word => { + // not implemented + // https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs#word-mode + }, + } + return; + } +}; diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index bc2dbb1a1..8ed291ef5 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -234,6 +234,74 @@ export default [ fn: "toBeOdd", length: 0, }, + toBeNil: { + fn: "toBeNil", + length: 0, + }, + toBeBoolean: { + fn: "toBeBoolean", + length: 0, + }, + toBeTrue: { + fn: "toBeTrue", + length: 0, + }, + toBeFalse: { + fn: "toBeFalse", + length: 0, + }, + toBeNumber: { + fn: "toBeNumber", + length: 0, + }, + toBeInteger: { + fn: "toBeInteger", + length: 0, + }, + toBeFinite: { + fn: "toBeFinite", + length: 0, + }, + toBePositive: { + fn: "toBePositive", + length: 0, + }, + toBeNegative: { + fn: "toBeNegative", + length: 0, + }, + toBeWithin: { + fn: "toBeWithin", + length: 2, + }, + toBeSymbol: { + fn: "toBeSymbol", + length: 0, + }, + toBeFunction: { + fn: "toBeFunction", + length: 0, + }, + toBeDate: { + fn: "toBeDate", + length: 0, + }, + toBeString: { + fn: "toBeString", + length: 0, + }, + toInclude: { + fn: "toInclude", + length: 1, + }, + toStartWith: { + fn: "toStartWith", + length: 1, + }, + toEndWith: { + fn: "toEndWith", + length: 1, + }, }, }), ]; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 58a6a3efe..a1260ecb1 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -10,7 +10,7 @@ const HTTPClient = @import("root").bun.HTTP; const NetworkThread = HTTPClient.NetworkThread; const Environment = @import("../../env.zig"); -const DiffMatchPatch = @import("../../deps/diffz/DiffMatchPatch.zig"); +const DiffFormatter = @import("./diff_format.zig").DiffFormatter; const JSC = @import("root").bun.JSC; const js = JSC.C; @@ -39,6 +39,7 @@ const JSValue = JSC.JSValue; const JSError = JSC.JSError; const JSGlobalObject = JSC.JSGlobalObject; const JSObject = JSC.JSObject; +const CallFrame = JSC.CallFrame; const VirtualMachine = JSC.VirtualMachine; const Task = @import("../javascript.zig").Task; @@ -46,297 +47,24 @@ const Task = @import("../javascript.zig").Task; const Fs = @import("../../fs.zig"); const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; -pub const DiffFormatter = struct { - received_string: ?string = null, - expected_string: ?string = null, - received: ?JSValue = null, - expected: ?JSValue = null, - globalObject: *JSC.JSGlobalObject, - not: bool = false, - - pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - if (this.expected_string != null and this.received_string != null) { - const received = this.received_string.?; - const expected = this.expected_string.?; - - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diff(default_allocator, received, expected, false); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d>{s}<r>"; - const delete_fmt = "<red>{s}<r>"; - const insert_fmt = "<green>{s}<r>"; - - try writer.writeAll("Expected: "); - for (diffs.items) |df| { - switch (df.operation) { - .delete => continue, - .insert => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - - try writer.writeAll("\nReceived: "); - for (diffs.items) |df| { - switch (df.operation) { - .insert => continue, - .delete => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - return; - } - - if (this.received == null or this.expected == null) return; - - const received = this.received.?; - const expected = this.expected.?; - var received_buf = MutableString.init(default_allocator, 0) catch unreachable; - var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; - defer { - received_buf.deinit(); - expected_buf.deinit(); - } - - { - var buffered_writer_ = bun.MutableString.BufferedWriter{ .context = &received_buf }; - var buffered_writer = &buffered_writer_; - - var buf_writer = buffered_writer.writer(); - const Writer = @TypeOf(buf_writer); - - const fmt_options = JSC.ZigConsoleClient.FormatOptions{ - .enable_colors = false, - .add_newline = false, - .flush = false, - .ordered_properties = true, - .quote_strings = true, - }; - JSC.ZigConsoleClient.format( - .Debug, - this.globalObject, - @ptrCast([*]const JSValue, &received), - 1, - Writer, - Writer, - buf_writer, - fmt_options, - ); - buffered_writer.flush() catch unreachable; - - buffered_writer_.context = &expected_buf; - - JSC.ZigConsoleClient.format( - .Debug, - this.globalObject, - @ptrCast([*]const JSValue, &this.expected), - 1, - Writer, - Writer, - buf_writer, - fmt_options, - ); - buffered_writer.flush() catch unreachable; - } - - const received_slice = received_buf.toOwnedSliceLeaky(); - const expected_slice = expected_buf.toOwnedSliceLeaky(); - - if (this.not) { - const not_fmt = "Expected: not <green>{s}<r>"; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(not_fmt, true), .{expected_slice}); - } else { - try writer.print(Output.prettyFmt(not_fmt, false), .{expected_slice}); - } - return; - } - - switch (received.determineDiffMethod(expected, this.globalObject)) { - .none => { - const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(fmt, true), .{ - expected.toFmt(this.globalObject, &formatter), - received.toFmt(this.globalObject, &formatter), - }); - return; - } - - try writer.print(Output.prettyFmt(fmt, true), .{ - expected.toFmt(this.globalObject, &formatter), - received.toFmt(this.globalObject, &formatter), - }); - return; - }, - .character => { - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diff(default_allocator, received_slice, expected_slice, false); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d>{s}<r>"; - const delete_fmt = "<red>{s}<r>"; - const insert_fmt = "<green>{s}<r>"; - - try writer.writeAll("Expected: "); - for (diffs.items) |df| { - switch (df.operation) { - .delete => continue, - .insert => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - - try writer.writeAll("\nReceived: "); - for (diffs.items) |df| { - switch (df.operation) { - .insert => continue, - .delete => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - return; - }, - .line => { - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diffLines(default_allocator, received_slice, expected_slice); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d> {s}<r>"; - const delete_fmt = "<red>+ {s}<r>"; - const insert_fmt = "<green>- {s}<r>"; - - var insert_count: usize = 0; - var delete_count: usize = 0; - - for (diffs.items) |df| { - var prev: usize = 0; - var curr: usize = 0; - switch (df.operation) { - .equal => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - .insert => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - insert_count += 1; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - .delete => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - delete_count += 1; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - } - if (df.text[df.text.len - 1] != '\n') try writer.writeAll("\n"); - } - - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt("\n<green>- Expected - {d}<r>\n", true), .{insert_count}); - try writer.print(Output.prettyFmt("<red>+ Received + {d}<r>", true), .{delete_count}); - return; - } - try writer.print("\n- Expected - {d}\n", .{insert_count}); - try writer.print("+ Received + {d}", .{delete_count}); - return; - }, - .word => { - // not implemented - // https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs#word-mode - }, - } - return; - } -}; - const ArrayIdentityContext = @import("../../identity_context.zig").ArrayIdentityContext; pub var test_elapsed_timer: ?*std.time.Timer = null; +pub const Tag = enum(u3) { + pass, + fail, + only, + skip, + todo, +}; + pub const TestRunner = struct { tests: TestRunner.Test.List = .{}, log: *logger.Log, files: File.List = .{}, index: File.Map = File.Map{}, only: bool = false, + run_todo: bool = false, last_file: u64 = 0, allocator: std.mem.Allocator, @@ -431,7 +159,6 @@ pub const TestRunner = struct { if (this.only) { return; } - this.only = true; var list = this.queue.readableSlice(0); @@ -461,6 +188,7 @@ pub const TestRunner = struct { this.tests.items(.status)[test_id] = .pass; this.callback.onTestPass(this.callback, test_id, file, label, expectations, elapsed_ns, parent); } + pub fn reportFailure(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void { this.tests.items(.status)[test_id] = .fail; this.callback.onTestFail(this.callback, test_id, file, label, expectations, elapsed_ns, parent); @@ -881,8 +609,8 @@ pub const Jest = struct { ); test_fn.put( globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), ); test_fn.put( globalObject, @@ -891,8 +619,18 @@ pub const Jest = struct { ); test_fn.put( globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ); + test_fn.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), + ); + test_fn.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), ); module.put( @@ -900,12 +638,32 @@ pub const Jest = struct { ZigString.static("it"), test_fn, ); - const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.describe, false); + const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); + describe.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), + ); describe.put( globalObject, ZigString.static("skip"), JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), ); + describe.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), + ); + describe.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), + ); + describe.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), + ); module.put( globalObject, @@ -1863,9 +1621,13 @@ pub const Expect = struct { var path_string = ZigString.Empty; expected_property_path.toZigString(&path_string, globalObject); - const received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + var pass = !value.isUndefinedOrNull(); + var received_property: JSValue = .zero; - var pass = !received_property.isEmpty(); + if (pass) { + received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + pass = !received_property.isEmpty(); + } if (pass and expected_property != null) { pass = received_property.deepEquals(expected_property.?, globalObject); @@ -3095,6 +2857,756 @@ pub const Expect = struct { return .zero; } + pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNil() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isUndefinedOrNull() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeBoolean() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isBoolean() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeTrue() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = (value.isBoolean() and value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFalse() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = (value.isBoolean() and !value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNumber() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isNumber() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeInteger() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isAnyInt() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFinite() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = std.math.isFinite(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBePositive() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNegative() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeWithin() must be called in a test", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + const startValue = arguments[0]; + startValue.ensureStillAlive(); + + if (!startValue.isNumber()) { + globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); + return .zero; + } + + const endValue = arguments[1]; + endValue.ensureStillAlive(); + + if (!endValue.isNumber()) { + globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num = value.asNumber(); + pass = num >= startValue.asNumber() and num < endValue.asNumber(); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const start_fmt = startValue.toFmt(globalThis, &formatter); + const end_fmt = endValue.toFmt(globalThis, &formatter); + const received_fmt = value.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected: not between <green>{any}<r> <d>(inclusive)<r> and <green>{any}<r> <d>(exclusive)<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toBeWithin", "<green>start<r><d>, <r><green>end<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + const expected_line = "Expected: between <green>{any}<r> <d>(inclusive)<r> and <green>{any}<r> <d>(exclusive)<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toBeWithin", "<green>start<r><d>, <r><green>end<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeSymbol() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isSymbol() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFunction() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isCallable(globalThis.vm()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeDate() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isDate() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeString() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isString() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toInclude() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toInclude() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.contains(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not include: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toInclude", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to include: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toInclude", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toStartWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toStartWith() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not start with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toStartWith", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to start with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toStartWith", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toEndWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toEndWith() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not end with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toEndWith", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to end with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toEndWith", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + pub const PropertyMatcherIterator = struct { received_object: JSValue, failed: bool, @@ -3389,136 +3901,39 @@ pub const TestScope = struct { promise: ?*JSInternalPromise = null, ran: bool = false, task: ?*TestRunnerTask = null, - skipped: bool = false, - is_todo: bool = false, + tag: Tag = .pass, snapshot_count: usize = 0, timeout_millis: u32 = 0, + retry_count: u32 = 0, // retry, on fail + repeat_count: u32 = 0, // retry, on pass or fail pub const Counter = struct { expected: u32 = 0, actual: u32 = 0, }; - pub fn only( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .only); - return thisValue; + pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test()", true, .pass); } - pub fn skip( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .skip); - return thisValue; + pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.only()", true, .only); } - pub fn call( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .call); - return thisValue; + pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.skip()", true, .skip); } - pub fn todo( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .todo); - return thisValue; + pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.todo()", true, .todo); } - inline fn prepare( - globalThis: *JSC.JSGlobalObject, - args: []const JSC.JSValue, - comptime tag: @Type(.EnumLiteral), - ) void { - var label: string = ""; - if (args.len == 0) { - return; - } - - var label_value = args[0]; - var function_value = if (args.len > 1) args[1] else JSC.JSValue.zero; - - if (label_value.isEmptyOrUndefinedOrNull() or !label_value.isString()) { - function_value = label_value; - label_value = .zero; - } - - if (label_value != .zero) { - const allocator = getAllocator(globalThis); - label = (label_value.toSlice(globalThis, allocator).cloneIfNeeded(allocator) catch unreachable).slice(); - } - - if (tag == .todo and label_value == .zero) { - globalThis.throw("test.todo() requires a description", .{}); - return; - } - - const function = function_value; - if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { - // a callback is not required for .todo - if (tag != .todo) { - globalThis.throw("test() expects a function", .{}); - return; - } - } - - if (tag == .only) { - Jest.runner.?.setOnly(); - } - - if (tag == .todo) { - if (function != .zero) - function.protect(); - DescribeScope.active.todo_counter += 1; - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .parent = DescribeScope.active, - .is_todo = true, - .callback = function, - }) catch unreachable; - - return; - } - - if (tag == .skip or (tag != .only and Jest.runner.?.only)) { - DescribeScope.active.skipped_counter += 1; - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .parent = DescribeScope.active, - .skipped = true, - .callback = .zero, - }) catch unreachable; - return; - } - - function.protect(); - - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .callback = function, - .parent = DescribeScope.active, - .timeout_millis = if (args.len > 2) @intCast(u32, @max(args[2].coerce(i32, globalThis), 0)) else Jest.runner.?.default_timeout_ms, - }) catch unreachable; + pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "test.if()", "if", TestScope, false); + } - if (test_elapsed_timer == null) create_tiemr: { - var timer = bun.default_allocator.create(std.time.Timer) catch unreachable; - timer.* = std.time.Timer.start() catch break :create_tiemr; - test_elapsed_timer = timer; - } + pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "test.skipIf()", "skipIf", TestScope, true); } pub fn onReject(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { @@ -3612,11 +4027,11 @@ pub const TestScope = struct { if (initial_value.isAnyError()) { if (!Jest.runner.?.did_pending_test_fail) { // test failed unless it's a todo - Jest.runner.?.did_pending_test_fail = !this.is_todo; + Jest.runner.?.did_pending_test_fail = this.tag != .todo; vm.runErrorHandler(initial_value, null); } - if (this.is_todo) { + if (this.tag == .todo) { return .{ .todo = {} }; } @@ -3639,11 +4054,11 @@ pub const TestScope = struct { .Rejected => { if (!Jest.runner.?.did_pending_test_fail) { // test failed unless it's a todo - Jest.runner.?.did_pending_test_fail = !this.is_todo; + Jest.runner.?.did_pending_test_fail = this.tag != .todo; vm.runErrorHandler(promise.result(vm.global.vm()), null); } - if (this.is_todo) { + if (this.tag == .todo) { return .{ .todo = {} }; } @@ -3713,12 +4128,12 @@ pub const DescribeScope = struct { current_test_id: TestRunner.Test.ID = 0, value: JSValue = .zero, done: bool = false, - skipped: bool = false, - skipped_counter: u32 = 0, - todo_counter: u32 = 0, + is_skip: bool = false, + skip_count: u32 = 0, + tag: Tag = .pass, pub fn isAllSkipped(this: *const DescribeScope) bool { - return this.skipped or @as(usize, this.skipped_counter) >= this.tests.items.len; + return this.is_skip or @as(usize, this.skip_count) >= this.tests.items.len; } pub fn push(new: *DescribeScope) void { @@ -3904,60 +4319,28 @@ pub const DescribeScope = struct { return this.execCallback(globalObject, hook); } - pub fn skip( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(3); - var this: *DescribeScope = DescribeScope.module; - return runDescribe(this, globalThis, arguments.ptr[0..arguments.len], true); + pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe()", false, .pass); } - pub fn describe( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(3); - var this: *DescribeScope = DescribeScope.module; - return runDescribe(this, globalThis, arguments.ptr[0..arguments.len], false); + pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.only()", false, .only); } - fn runDescribe( - this: *DescribeScope, - globalThis: *JSC.JSGlobalObject, - arguments: []const JSC.JSValue, - skipped: bool, - ) JSC.JSValue { - if (arguments.len == 0 or arguments.len > 2) { - globalThis.throwNotEnoughArguments("describe", 2, arguments.len); - return .zero; - } - - var label = ZigString.init(""); - var args = arguments; - const allocator = getAllocator(globalThis); - - if (arguments[0].isString()) { - arguments[0].toZigString(&label, globalThis); - args = args[1..]; - } - - if (args.len == 0 or !args[0].isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType("describe", "callback", "function"); - return .zero; - } + pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.skip()", false, .skip); + } - var callback = args[0]; + pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.todo()", false, .todo); + } - var scope = allocator.create(DescribeScope) catch unreachable; - scope.* = .{ - .label = (label.toSlice(allocator).cloneIfNeeded(allocator) catch unreachable).slice(), - .parent = active, - .file_id = this.file_id, - .skipped = skipped or active.skipped, - }; + pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "describe.if()", "if", DescribeScope, false); + } - return scope.run(globalThis, callback); + pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "describe.skipIf()", "skipIf", DescribeScope, true); } pub fn run(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, callback: JSC.JSValue) JSC.JSValue { @@ -3970,6 +4353,11 @@ pub const DescribeScope = struct { this.parent = this.parent orelse active; active = this; + if (callback == .zero) { + this.runTests(globalObject); + return .undefined; + } + { JSC.markBinding(@src()); globalObject.clearTerminationException(); @@ -4004,7 +4392,10 @@ pub const DescribeScope = struct { const end = @truncate(TestRunner.Test.ID, tests.len); this.pending_tests = std.DynamicBitSetUnmanaged.initFull(allocator, end) catch unreachable; - if (end == 0) return; + if (end == 0) { + // TODO: print the describe label when there are no tests + return; + } // Step 2. Update the runner with the count of how many tests we have for this block this.test_id_start = Jest.runner.?.addTestCount(end); @@ -4151,19 +4542,25 @@ pub const TestRunnerTask = struct { var test_: TestScope = this.describe.tests.items[test_id]; describe.current_test_id = test_id; - if (!describe.skipped and test_.is_todo and test_.callback.isEmpty()) { - this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe); - this.deinit(); - return false; - } - - if (test_.skipped or describe.skipped) { - this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe); + if (test_.callback == .zero or (describe.is_skip and test_.tag != .only)) { + var tag = if (describe.is_skip) describe.tag else test_.tag; + switch (tag) { + .todo => { + this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe); + }, + .skip => { + this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe); + }, + else => {}, + } this.deinit(); return false; } jsc_vm.onUnhandledRejectionCtx = this; + if (Output.is_github_action) { + jsc_vm.setOnException(printGithubAnnotation); + } if (this.needs_before_each) { this.needs_before_each = false; @@ -4259,7 +4656,7 @@ pub const TestRunnerTask = struct { } fn processTestResult(this: *TestRunnerTask, globalThis: *JSC.JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void { - switch (result.forceTODO(test_.is_todo)) { + switch (result.forceTODO(test_.tag == .todo)) { .pass => |count| Jest.runner.?.reportPass( test_id, this.source_file_path, @@ -4311,6 +4708,7 @@ pub const TestRunnerTask = struct { vm.onUnhandledRejectionCtx = null; } } + vm.clearOnException(); this.ref.unref(vm); @@ -4344,3 +4742,268 @@ pub const Result = union(TestRunner.Test.Status) { return this; } }; + +inline fn createScope( + globalThis: *JSGlobalObject, + callframe: *CallFrame, + comptime signature: string, + comptime is_test: bool, + comptime tag: Tag, +) JSValue { + const this = callframe.this(); + const arguments = callframe.arguments(3); + const args = arguments.ptr[0..arguments.len]; + + if (args.len == 0) { + globalThis.throwPretty("{s} expects a description or function", .{signature}); + return .zero; + } + + var description = args[0]; + var function = if (args.len > 1) args[1] else .zero; + var options = if (args.len > 2) args[2] else .zero; + + if (description.isEmptyOrUndefinedOrNull() or !description.isString()) { + function = description; + description = .zero; + } + + if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { + if (tag != .todo) { + globalThis.throwPretty("{s} expects a function", .{signature}); + return .zero; + } + } + + var timeout_ms: u32 = Jest.runner.?.default_timeout_ms; + if (options.isNumber()) { + timeout_ms = @intCast(u32, @max(args[2].coerce(i32, globalThis), 0)); + } else if (options.isObject()) { + if (options.get(globalThis, "timeout")) |timeout| { + if (!timeout.isNumber()) { + globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); + return .zero; + } + timeout_ms = @intCast(u32, @max(timeout.coerce(i32, globalThis), 0)); + } + if (options.get(globalThis, "retry")) |retries| { + if (!retries.isNumber()) { + globalThis.throwPretty("{s} expects retry to be a number", .{signature}); + return .zero; + } + // TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0)); + } + if (options.get(globalThis, "repeats")) |repeats| { + if (!repeats.isNumber()) { + globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); + return .zero; + } + // TODO: repeat_count = @intCast(u32, @max(repeats.coerce(i32, globalThis), 0)); + } + } else if (!options.isEmptyOrUndefinedOrNull()) { + globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); + return .zero; + } + + const parent = DescribeScope.active; + const allocator = getAllocator(globalThis); + const label = if (description == .zero) + "" + else + (description.toSlice(globalThis, allocator).cloneIfNeeded(allocator) catch unreachable).slice(); + + if (tag == .only) { + Jest.runner.?.setOnly(); + } else if (is_test and Jest.runner.?.only and parent.tag != .only) { + return .zero; + } + + const is_skip = tag == .skip or + (tag == .todo and (function == .zero or !Jest.runner.?.run_todo)) or + (tag != .only and Jest.runner.?.only and parent.tag != .only); + + if (is_test) { + if (is_skip) { + parent.skip_count += 1; + function.unprotect(); + } else { + function.protect(); + } + + parent.tests.append(allocator, TestScope{ + .label = label, + .parent = parent, + .tag = tag, + .callback = if (is_skip) .zero else function, + .timeout_millis = timeout_ms, + }) catch unreachable; + + if (test_elapsed_timer == null) create_timer: { + var timer = allocator.create(std.time.Timer) catch unreachable; + timer.* = std.time.Timer.start() catch break :create_timer; + test_elapsed_timer = timer; + } + } else { + var scope = allocator.create(DescribeScope) catch unreachable; + scope.* = .{ + .label = label, + .parent = parent, + .file_id = parent.file_id, + .tag = if (parent.is_skip) parent.tag else tag, + .is_skip = is_skip or parent.is_skip, + }; + + return scope.run(globalThis, function); + } + + return this; +} + +inline fn createIfScope( + globalThis: *JSGlobalObject, + callframe: *CallFrame, + comptime property: string, + comptime signature: string, + comptime Scope: type, + comptime is_skip: bool, +) JSValue { + const arguments = callframe.arguments(1); + const args = arguments.ptr[0..arguments.len]; + + if (args.len == 0) { + globalThis.throwPretty("{s} expects a condition", .{signature}); + return .zero; + } + + const name = ZigString.static(property); + const value = args[0].toBooleanSlow(globalThis); + const skip = if (is_skip) Scope.skip else Scope.call; + const call = if (is_skip) Scope.call else Scope.skip; + + if (value) { + return JSC.NewFunction(globalThis, name, 2, skip, false); + } + + return JSC.NewFunction(globalThis, name, 2, call, false); +} + +// In Github Actions, emit an annotation that renders the error and location. +// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message +pub fn printGithubAnnotation(exception: *JSC.ZigException) void { + const name = exception.name; + const message = exception.message; + const frames = exception.stack.frames(); + const top_frame = if (frames.len > 0) frames[0] else null; + const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; + const allocator = bun.default_allocator; + + var has_location = false; + + if (top_frame) |frame| { + if (!frame.position.isInvalid()) { + const source_url = frame.source_url.toSlice(allocator); + defer source_url.deinit(); + const file = bun.path.relative(dir, source_url.slice()); + Output.printError("\n::error file={s},line={d},col={d},title=", .{ + file, + frame.position.line_start + 1, + frame.position.column_start, + }); + has_location = true; + } + } + + if (!has_location) { + Output.printError("\n::error title=", .{}); + } + + if (name.len == 0 or name.eqlComptime("Error")) { + Output.printError("error", .{}); + } else { + Output.printError("{s}", .{name.githubAction()}); + } + + if (message.len > 0) { + const message_slice = message.toSlice(allocator); + defer message_slice.deinit(); + const msg = message_slice.slice(); + + var cursor: u32 = 0; + while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { + cursor = i + 1; + if (msg[i] == '\n') { + const first_line = ZigString.init(msg[0..i]); + Output.printError(": {s}::", .{first_line.githubAction()}); + break; + } + } else { + Output.printError(": {s}::", .{message.githubAction()}); + } + + while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { + cursor = i + 1; + if (msg[i] == '\n') { + break; + } + } + + if (cursor > 0) { + const body = ZigString.init(msg[cursor..]); + Output.printError("{s}", .{body.githubAction()}); + } + } else { + Output.printError("::", .{}); + } + + // TODO: cleanup and refactor to use printStackTrace() + if (top_frame) |_| { + const vm = VirtualMachine.get(); + const origin = if (vm.is_from_devserver) &vm.origin else null; + + var i: i16 = 0; + while (i < frames.len) : (i += 1) { + const frame = frames[@intCast(usize, i)]; + const source_url = frame.source_url.toSlice(allocator); + defer source_url.deinit(); + const file = bun.path.relative(dir, source_url.slice()); + const func = frame.function_name.toSlice(allocator); + + if (file.len == 0 and func.len == 0) continue; + + const has_name = std.fmt.count("{any}", .{frame.nameFormatter( + false, + )}) > 0; + + // %0A = escaped newline + if (has_name) { + Output.printError( + "%0A at {any} ({any})", + .{ + frame.nameFormatter(false), + frame.sourceURLFormatter( + file, + origin, + false, + false, + ), + }, + ); + } else { + Output.printError( + "%0A at {any}", + .{ + frame.sourceURLFormatter( + file, + origin, + false, + false, + ), + }, + ); + } + } + } + + Output.printError("\n", .{}); + Output.flush(); +} diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 47a267b7f..0431b2e10 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -983,38 +983,35 @@ pub const JestPrettyFormat = struct { defer if (comptime enable_ansi_colors) writer.writeAll(Output.prettyFmt("<r>", true)); - if (str.is16Bit()) { - this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); - return; - } - var has_newline = false; - if (strings.indexOfAny(str.slice(), "\n\r")) |_| { + + if (str.indexOfAny("\n\r")) |_| { has_newline = true; writer.writeAll("\n"); } writer.writeAll("\""); - var remaining = str.slice(); - while (strings.indexOfAny(remaining, "\\\r")) |i| { - switch (remaining[i]) { + var remaining = str; + while (remaining.indexOfAny("\\\r")) |i| { + switch (remaining.charAt(i)) { '\\' => { - writer.print("{s}\\", .{remaining[0 .. i + 1]}); - remaining = remaining[i + 1 ..]; + writer.print("{}\\", .{remaining.substringWithLen(0, i)}); + remaining = remaining.substring(i + 1, 0); }, '\r' => { - if (i + 1 < remaining.len and remaining[i + 1] == '\n') { - writer.print("{s}", .{remaining[0..i]}); + if (i + 1 < remaining.len and remaining.charAt(i + 1) == '\n') { + writer.print("{}", .{remaining.substringWithLen(0, i)}); } else { - writer.print("{s}\n", .{remaining[0..i]}); + writer.print("{}\n", .{remaining.substringWithLen(0, i)}); } - remaining = remaining[i + 1 ..]; + + remaining = remaining.substring(i + 1, 0); }, else => unreachable, } } - writer.writeAll(remaining); + writer.writeString(remaining); writer.writeAll("\""); if (has_newline) writer.writeAll("\n"); return; @@ -1026,7 +1023,7 @@ pub const JestPrettyFormat = struct { if (str.is16Bit()) { // streaming print - writer.print("{s}", .{str}); + writer.print("{}", .{str}); } else if (strings.isAllASCII(str.slice())) { // fast path writer.writeAll(str.slice()); diff --git a/src/cli.zig b/src/cli.zig index dc1ae0cdc..0d4c32cdf 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -215,6 +215,8 @@ pub const Arguments = struct { clap.parseParam("--timeout <NUMBER> Set the per-test timeout in milliseconds, default is 5000.") catch unreachable, clap.parseParam("--update-snapshots Update snapshot files") catch unreachable, clap.parseParam("--rerun-each <NUMBER> Re-run each test file <NUMBER> times, helps catch certain bugs") catch unreachable, + clap.parseParam("--only Only run tests that are marked with \"test.only()\"") catch unreachable, + clap.parseParam("--todo Include tests that are marked with \"test.todo()\"") catch unreachable, }; const build_params_public = public_params ++ build_only_params; @@ -390,6 +392,8 @@ pub const Arguments = struct { }; } } + ctx.test_options.run_todo = args.flag("--todo"); + ctx.test_options.only = args.flag("--only"); } ctx.args.absolute_working_dir = cwd; @@ -923,6 +927,8 @@ pub const Command = struct { default_timeout_ms: u32 = 5 * std.time.ms_per_s, update_snapshots: bool = false, repeat_count: u32 = 0, + run_todo: bool = false, + only: bool = false, }; pub const Context = struct { diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 0e337ebf0..b7712c0de 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -48,12 +48,21 @@ const uws = @import("root").bun.uws; fn fmtStatusTextLine(comptime status: @Type(.EnumLiteral), comptime emoji: bool) []const u8 { comptime { - return switch (status) { - .pass => Output.prettyFmt("<r><green>✓<r>", emoji), - .fail => Output.prettyFmt("<r><red>✗<r>", emoji), - .skip => Output.prettyFmt("<r><yellow>-<d>", emoji), - .todo => Output.prettyFmt("<r><magenta>✎<r>", emoji), - else => @compileError("Invalid status " ++ @tagName(status)), + return switch (emoji) { + true => switch (status) { + .pass => Output.prettyFmt("<r><green>✓<r>", true), + .fail => Output.prettyFmt("<r><red>✗<r>", true), + .skip => Output.prettyFmt("<r><yellow>»<d>", true), + .todo => Output.prettyFmt("<r><magenta>✎<r>", true), + else => @compileError("Invalid status " ++ @tagName(status)), + }, + else => switch (status) { + .pass => Output.prettyFmt("<r><green>(pass)<r>", true), + .fail => Output.prettyFmt("<r><red>(fail)<r>", true), + .skip => Output.prettyFmt("<r><yellow>(skip)<d>", true), + .todo => Output.prettyFmt("<r><magenta>(todo)<r>", true), + else => @compileError("Invalid status " ++ @tagName(status)), + }, }; } } @@ -94,13 +103,9 @@ pub const CommandLineReporter = struct { break :brk map; }; - pub fn handleUpdateCount(cb: *TestRunner.Callback, _: u32, _: u32) void { - _ = cb; - } + pub fn handleUpdateCount(_: *TestRunner.Callback, _: u32, _: u32) void {} - pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void { - // var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb); - } + pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void {} fn printTestLine(label: string, elapsed_ns: u64, parent: ?*jest.DescribeScope, comptime skip: bool, writer: anytype) void { var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; @@ -329,6 +334,16 @@ const Scanner = struct { if (this.filter_names.len == 0) return true; for (this.filter_names) |filter_name| { + if (strings.startsWith(name, filter_name)) return true; + } + + return false; + } + + pub fn doesPathMatchFilter(this: *Scanner, name: string) bool { + if (this.filter_names.len == 0) return true; + + for (this.filter_names) |filter_name| { if (strings.contains(name, filter_name)) return true; } @@ -336,7 +351,7 @@ const Scanner = struct { } pub fn isTestFile(this: *Scanner, name: string) bool { - return this.couldBeTestFile(name) and this.doesAbsolutePathMatchFilter(name); + return this.couldBeTestFile(name) and this.doesPathMatchFilter(name); } pub fn next(this: *Scanner, entry: *FileSystem.Entry, fd: bun.StoredFileDescriptorType) void { @@ -370,7 +385,10 @@ const Scanner = struct { var parts = &[_]string{ entry.dir, entry.base() }; const path = this.fs.absBuf(parts, &this.open_dir_buf); - if (!this.doesAbsolutePathMatchFilter(path)) return; + if (!this.doesAbsolutePathMatchFilter(path)) { + const rel_path = bun.path.relative(this.fs.top_level_dir, path); + if (!this.doesPathMatchFilter(rel_path)) return; + } entry.abs_path = bun.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable); this.results.append(entry.abs_path) catch unreachable; @@ -385,6 +403,9 @@ pub const TestCommand = struct { pub fn exec(ctx: Command.Context) !void { if (comptime is_bindgen) unreachable; + + Output.is_github_action = Output.isGithubAction(); + // print the version so you know its doing stuff if it takes a sec if (strings.eqlComptime(ctx.positionals[0], old_name)) { Output.prettyErrorln("<r><b>bun wiptest <r><d>v" ++ Global.package_json_version_with_sha ++ "<r>", .{}); @@ -415,6 +436,8 @@ pub const TestCommand = struct { .log = ctx.log, .callback = undefined, .default_timeout_ms = ctx.test_options.default_timeout_ms, + .run_todo = ctx.test_options.run_todo, + .only = ctx.test_options.only, .snapshots = Snapshots{ .allocator = ctx.allocator, .update_snapshots = ctx.test_options.update_snapshots, @@ -717,14 +740,26 @@ pub const TestCommand = struct { var resolution = try vm.bundler.resolveEntryPoint(file_name); vm.clearEntryPoint(); - Output.prettyErrorln("<r>\n{s}:\n", .{resolution.path_pair.primary.name.filename}); - Output.flush(); + const file_path = resolution.path_pair.primary.text; + const file_title = bun.path.relative(FileSystem.instance.top_level_dir, file_path); + + // In Github Actions, append a special prefix that will group + // subsequent log lines into a collapsable group. + // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines + const file_prefix = if (Output.is_github_action) "::group::" else ""; - vm.main_hash = @truncate(u32, bun.hash(resolution.path_pair.primary.text)); + vm.main_hash = @truncate(u32, bun.hash(file_path)); var repeat_count = reporter.repeat_count; var repeat_index: u32 = 0; while (repeat_index < repeat_count) : (repeat_index += 1) { - var promise = try vm.loadEntryPoint(resolution.path_pair.primary.text); + if (repeat_count > 1) { + Output.prettyErrorln("<r>\n{s}{s}: <d>(run #{d})<r>\n", .{ file_prefix, file_title, repeat_index + 1 }); + } else { + Output.prettyErrorln("<r>\n{s}{s}:\n", .{ file_prefix, file_title }); + } + Output.flush(); + + var promise = try vm.loadEntryPoint(file_path); switch (promise.status(vm.global.vm())) { .Rejected => { @@ -789,9 +824,12 @@ pub const TestCommand = struct { vm.global.handleRejectedPromises(); if (repeat_index > 0) { vm.clearEntryPoint(); - var entry = JSC.ZigString.init(resolution.path_pair.primary.text); + var entry = JSC.ZigString.init(file_path); vm.global.deleteModuleRegistryEntry(&entry); - Output.prettyErrorln("<r>{s} <d>[RUN {d:0>4}]:<r>\n", .{ resolution.path_pair.primary.name.filename, repeat_index + 1 }); + } + + if (Output.is_github_action) { + Output.prettyErrorln("<r>\n::endgroup::\n", .{}); Output.flush(); } } diff --git a/src/output.zig b/src/output.zig index 4f47d2496..a37a58abf 100644 --- a/src/output.zig +++ b/src/output.zig @@ -168,6 +168,7 @@ pub var enable_ansi_colors = Environment.isNative; pub var enable_ansi_colors_stderr = Environment.isNative; pub var enable_ansi_colors_stdout = Environment.isNative; pub var enable_buffering = Environment.isNative; +pub var is_github_action = false; pub var stderr_descriptor_type = OutputStreamDescriptor.unknown; pub var stdout_descriptor_type = OutputStreamDescriptor.unknown; @@ -176,6 +177,13 @@ pub inline fn isEmojiEnabled() bool { return enable_ansi_colors and !Environment.isWindows; } +pub fn isGithubAction() bool { + if (bun.getenvZ("GITHUB_ACTIONS")) |value| { + return strings.eqlComptime(value, "true"); + } + return false; +} + var _source_for_test: if (Environment.isTest) Output.Source else void = undefined; var _source_for_test_set = false; pub fn initTest() void { diff --git a/src/string_immutable.zig b/src/string_immutable.zig index f7e7a3657..25d4fb01a 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -38,7 +38,7 @@ pub fn toUTF16Literal(comptime str: []const u8) []const u16 { }; } -const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); +pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); pub fn indexOfAny(self: string, comptime str: anytype) ?OptionalUsize { inline for (str) |a| { if (indexOfChar(self, a)) |i| { @@ -689,6 +689,63 @@ pub fn endsWithAny(self: string, str: string) bool { return false; } +// Formats a string to be safe to output in a Github action. +// - Encodes "\n" as "%0A" to support multi-line strings. +// https://github.com/actions/toolkit/issues/193#issuecomment-605394935 +// - Strips ANSI output as it will appear malformed. +pub fn githubActionWriter(writer: anytype, self: string) !void { + var offset: usize = 0; + const end = @truncate(u32, self.len); + while (offset < end) { + if (indexOfNewlineOrNonASCIIOrANSI(self, @truncate(u32, offset))) |i| { + const byte = self[i]; + if (byte > 0x7F) { + offset += @max(wtf8ByteSequenceLength(byte), 1); + continue; + } + if (i > 0) { + try writer.writeAll(self[offset..i]); + } + var n: usize = 1; + if (byte == '\n') { + try writer.writeAll("%0A"); + } else if (i + 1 < end) { + const next = self[i + 1]; + if (byte == '\r' and next == '\n') { + n += 1; + try writer.writeAll("%0A"); + } else if (byte == '\x1b' and next == '[') { + n += 1; + if (i + 2 < end) { + const remain = self[(i + 2)..@min(i + 5, end)]; + if (indexOfChar(remain, 'm')) |j| { + n += j + 1; + } + } + } + } + offset = i + n; + } else { + try writer.writeAll(self[offset..end]); + break; + } + } +} + +pub const GithubActionFormatter = struct { + text: string, + + pub fn format(this: GithubActionFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try githubActionWriter(writer, this.text); + } +}; + +pub fn githubAction(self: string) strings.GithubActionFormatter { + return GithubActionFormatter{ + .text = self, + }; +} + pub fn quotedWriter(writer: anytype, self: string) !void { var remain = self; if (strings.containsNewlineOrNonASCIIOrQuote(remain)) { @@ -3179,6 +3236,44 @@ pub fn firstNonASCIIWithType(comptime Type: type, slice: Type) ?u32 { return null; } +pub fn indexOfNewlineOrNonASCIIOrANSI(slice_: []const u8, offset: u32) ?u32 { + const slice = slice_[offset..]; + var remaining = slice; + + if (remaining.len == 0) + return null; + + if (comptime Environment.enableSIMD) { + while (remaining.len >= ascii_vector_size) { + const vec: AsciiVector = remaining[0..ascii_vector_size].*; + const cmp = @bitCast(AsciiVectorU1, (vec > max_16_ascii)) | @bitCast(AsciiVectorU1, (vec < min_16_ascii)) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\r'))) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\n'))) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\x1b'))); + + if (@reduce(.Max, cmp) > 0) { + const bitmask = @bitCast(AsciiVectorInt, cmp); + const first = @ctz(bitmask); + + return @as(u32, first) + @intCast(u32, slice.len - remaining.len) + offset; + } + + remaining = remaining[ascii_vector_size..]; + } + + if (comptime Environment.allow_assert) std.debug.assert(remaining.len < ascii_vector_size); + } + + for (remaining) |*char_| { + const char = char_.*; + if (char > 127 or char < 0x20 or char == '\n' or char == '\r' or char == '\x1b') { + return @truncate(u32, (@ptrToInt(char_) - @ptrToInt(slice.ptr))) + offset; + } + } + + return null; +} + pub fn indexOfNewlineOrNonASCII(slice_: []const u8, offset: u32) ?u32 { return indexOfNewlineOrNonASCIICheckStart(slice_, offset, true); } diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index e1a5fdd60..788ea2454 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -1,11 +1,184 @@ -import { join } from "node:path"; +import { join, resolve, dirname } from "node:path"; import { tmpdir } from "node:os"; -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs"; import { spawnSync } from "bun"; import { describe, test, expect } from "bun:test"; import { bunExe, bunEnv } from "harness"; describe("bun test", () => { + test("can provide no arguments", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test("test #1", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test.todo("test #2"); + `, + ` + import { test, expect } from "bun:test"; + test("test #3", () => { + expect(true).toBe(false); + }); + `, + ], + }); + expect(stderr).toContain("test #1"); + expect(stderr).toContain("test #2"); + expect(stderr).toContain("test #3"); + }); + test("can provide a relative file", () => { + const path = join("path", "to", "relative.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const stderr = runTest({ + cwd, + args: [path], + }); + expect(stderr).toContain(path); + }); + // This fails on macOS because /private/var symlinks to /var + test.todo("can provide an absolute file", () => { + const path = join("path", "to", "absolute.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const absolutePath = resolve(cwd, path); + const stderr = runTest({ + cwd, + args: [absolutePath], + }); + expect(stderr).toContain(path); + }); + test("can provide a relative directory", () => { + const path = join("path", "to", "relative.test.ts"); + const dir = dirname(path); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${dir}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const stderr = runTest({ + cwd, + args: [dir], + }); + expect(stderr).toContain(dir); + }); + test.todo("can provide an absolute directory", () => { + const path = join("path", "to", "absolute.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const absoluteDir = resolve(cwd, dirname(path)); + const stderr = runTest({ + cwd, + args: [absoluteDir], + }); + expect(stderr).toContain(path); + }); + test.todo("can provide a mix of files and directories"); + describe("--rerun-each", () => { + test.todo("can rerun with a default value"); + test.todo("can rerun with a provided value"); + }); + describe("--todo", () => { + test("should not run todo by default", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test.todo("todo", async () => { + console.error("should not run"); + }); + `, + }); + expect(stderr).not.toContain("should not run"); + }); + test("should run todo when enabled", () => { + const stderr = runTest({ + args: ["--todo"], + input: ` + import { test, expect } from "bun:test"; + test.todo("todo", async () => { + console.error("should run"); + }); + `, + }); + expect(stderr).toContain("should run"); + }); + }); + describe("--only", () => { + test("should skip non-only tests when enabled", () => { + const stderr = runTest({ + args: ["--only"], + input: ` + import { test, describe } from "bun:test"; + test("test #1", () => { + console.error("unreachable"); + }); + test.only("test #2", () => { + console.error("reachable"); + }); + test("test #3", () => { + console.error("unreachable"); + }); + test.skip("test #4", () => { + console.error("unreachable"); + }); + test.todo("test #5"); + describe("describe #1", () => { + test("test #6", () => { + console.error("unreachable"); + }); + test.only("test #7", () => { + console.error("reachable"); + }); + }); + describe.only("describe #2", () => { + test("test #8", () => { + console.error("reachable"); + }); + test.skip("test #9", () => { + console.error("unreachable"); + }); + test.only("test #10", () => { + console.error("reachable"); + }); + }); + `, + }); + expect(stderr).toContain("reachable"); + expect(stderr).not.toContain("unreachable"); + expect(stderr.match(/reachable/g)).toHaveLength(4); + }); + }); describe("--timeout", () => { test("must provide a number timeout", () => { const stderr = runTest({ @@ -22,7 +195,7 @@ describe("bun test", () => { test("timeout can be set to 1ms", () => { const stderr = runTest({ args: ["--timeout", "1"], - code: ` + input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; test("timeout", async () => { @@ -34,7 +207,7 @@ describe("bun test", () => { }); test("timeout should default to 5000ms", () => { const stderr = runTest({ - code: ` + input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; test("timeout", async () => { @@ -45,22 +218,207 @@ describe("bun test", () => { expect(stderr).toContain("timed out after 5000ms"); }); }); + describe("support for Github Actions", () => { + test("should not group logs by default", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::group::"); + expect(stderr).not.toContain("::endgroup::"); + }); + test("should not group logs when disabled", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: "false", + }, + }); + expect(stderr).not.toContain("::group::"); + expect(stderr).not.toContain("::endgroup::"); + }); + test("should group logs when enabled", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(1); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(1); + }); + test("should group logs with multiple files", () => { + const stderr = runTest({ + input: [ + ` + import { test, expect } from "bun:test"; + test("pass", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test.skip("skip", () => {}); + `, + ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + ], + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(3); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(3); + }); + test("should group logs with --rerun-each", () => { + const stderr = runTest({ + args: ["--rerun-each", "3"], + input: [ + ` + import { test, expect } from "bun:test"; + test("pass", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + ], + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(6); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(6); + }); + test("should not annotate errors by default", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::error"); + }); + test("should not annotate errors when using inspect()", () => { + const stderr = runTest({ + input: ` + import { test } from "bun:test"; + import { inspect } from "bun"; + test("inspect", () => { + inspect(new TypeError()); + console.error(inspect(new TypeError())); + }); + `, + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::error"); + }); + test("should annotate errors when enabled", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + throw new Error(); + }); + `, + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=error::/); + }); + test("should annotate errors with escaped strings", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + env: { + FORCE_COLOR: "1", + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=.*::/); + expect(stderr).toMatch(/error: expect\(received\)\.toBe\(expected\)/); // stripped ansi + expect(stderr).toMatch(/Expected: false%0AReceived: true%0A/); // escaped newlines + }); + test("should annotate errors without a stack", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + throw "Oops!"; + }); + `, + env: { + FORCE_COLOR: "1", + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error title=error: Oops!::/); + }); + }); }); -function runTest({ code = "", args = [] }: { code?: string; args?: string[] }): string { - const dir = mkdtempSync(join(tmpdir(), "bun-test-")); - const path = join(dir, `bun-test-${Date.now()}.test.ts`); - writeFileSync(path, code); +function createTest(input?: string | string[], filename?: string): string { + const cwd = mkdtempSync(join(tmpdir(), "bun-test-")); + const inputs = Array.isArray(input) ? input : [input ?? ""]; + for (const input of inputs) { + const path = join(cwd, filename ?? `bun-test-${Math.random()}.test.ts`); + try { + writeFileSync(path, input); + } catch { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, input); + } + } + return cwd; +} + +function runTest({ + input = "", + cwd, + args = [], + env = {}, +}: { + input?: string | string[]; + cwd?: string; + args?: string[]; + env?: Record<string, string | undefined>; +} = {}): string { + cwd ??= createTest(input); try { const { stderr } = spawnSync({ - cwd: dir, - cmd: [bunExe(), "test", path, ...args], - env: bunEnv, + cwd, + cmd: [bunExe(), "test", ...args], + env: { ...bunEnv, ...env }, stderr: "pipe", stdout: "ignore", }); return stderr.toString(); } finally { - rmSync(path); + rmSync(cwd, { recursive: true }); } } diff --git a/test/harness.ts b/test/harness.ts index d29e4367c..bfd159b18 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -6,6 +6,7 @@ import os from "os"; export const bunEnv: any = { ...process.env, + GITHUB_ACTIONS: "false", BUN_DEBUG_QUIET_LOGS: "1", NO_COLOR: "1", FORCE_COLOR: undefined, diff --git a/test/js/bun/test/__snapshots__/test-test.test.ts.snap b/test/js/bun/test/__snapshots__/test-test.test.ts.snap index 31c5d310b..7fea0e8d3 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -1,3 +1,45 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`expect().toEqual() on objects with property indices doesn't print undefined 1`] = `"expect(received).toEqual(expected)\n\n {\n+ \"0\": 0,\n+ \"1\": 1,\n+ \"10\": 10,\n+ \"11\": 11,\n+ \"12\": 12,\n+ \"13\": 13,\n+ \"14\": 14,\n+ \"15\": 15,\n+ \"2\": 2,\n+ \"3\": 3,\n+ \"4\": 4,\n+ \"5\": 5,\n+ \"6\": 6,\n+ \"7\": 7,\n+ \"8\": 8,\n+ \"9\": 9\n- \"0\": 123,\n- \"1\": 123,\n- \"10\": 123,\n- \"11\": 123,\n- \"12\": 123,\n- \"13\": 123,\n- \"14\": 123,\n- \"15\": 123,\n- \"2\": 123,\n- \"3\": 123,\n- \"4\": 123,\n- \"5\": 123,\n- \"6\": 123,\n- \"7\": 123,\n- \"8\": 123,\n- \"9\": 123\n }\n\n- Expected - 16\n+ Received + 16\n\n "`; +exports[`expect().toEqual() on objects with property indices doesn't print undefined 1`] = ` +"expect(received).toEqual(expected) + + { ++ "0": 0, ++ "1": 1, ++ "10": 10, ++ "11": 11, ++ "12": 12, ++ "13": 13, ++ "14": 14, ++ "15": 15, ++ "2": 2, ++ "3": 3, ++ "4": 4, ++ "5": 5, ++ "6": 6, ++ "7": 7, ++ "8": 8, ++ "9": 9 +- "0": 123, +- "1": 123, +- "10": 123, +- "11": 123, +- "12": 123, +- "13": 123, +- "14": 123, +- "15": 123, +- "2": 123, +- "3": 123, +- "4": 123, +- "5": 123, +- "6": 123, +- "7": 123, +- "8": 123, +- "9": 123 + } + +- Expected - 16 ++ Received + 16 + + " +`; diff --git a/test/js/bun/test/expect.test.ts b/test/js/bun/test/expect.test.ts index 96896013b..2afb9726c 100644 --- a/test/js/bun/test/expect.test.ts +++ b/test/js/bun/test/expect.test.ts @@ -192,6 +192,185 @@ describe("expect()", () => { }); } }); + + test("toBeNil()", () => { + expect(null).toBeNil(); + expect(undefined).toBeNil(); + expect(false).not.toBeNil(); + expect(0).not.toBeNil(); + expect("").not.toBeNil(); + expect([]).not.toBeNil(); + expect(true).not.toBeNil(); + expect({}).not.toBeNil(); + }); + + test("toBeBoolean()", () => { + expect(true).toBeBoolean(); + expect(false).toBeBoolean(); + expect(0).not.toBeBoolean(); + expect(1).not.toBeBoolean(); + expect("").not.toBeBoolean(); + expect({}).not.toBeBoolean(); + }); + + test("toBeTrue()", () => { + expect(true).toBeTrue(); + expect(false).not.toBeTrue(); + expect(0).not.toBeTrue(); + expect(1).not.toBeTrue(); + expect("").not.toBeTrue(); + expect({}).not.toBeTrue(); + }); + + test("toBeFalse()", () => { + expect(false).toBeFalse(); + expect(true).not.toBeFalse(); + expect(0).not.toBeFalse(); + expect(1).not.toBeFalse(); + expect("").not.toBeFalse(); + expect({}).not.toBeFalse(); + }); + + test("toBeNumber()", () => { + expect(0).toBeNumber(); + expect(1).toBeNumber(); + expect(1.23).toBeNumber(); + expect(Infinity).toBeNumber(); + expect(-Infinity).toBeNumber(); + expect(NaN).toBeNumber(); + expect("").not.toBeNumber(); + expect({}).not.toBeNumber(); + }); + + test("toBeInteger()", () => { + expect(0).toBeInteger(); + expect(1).toBeInteger(); + expect(1.23).not.toBeInteger(); + expect(Infinity).not.toBeInteger(); + expect(-Infinity).not.toBeInteger(); + expect(NaN).not.toBeInteger(); + expect("").not.toBeInteger(); + expect({}).not.toBeInteger(); + }); + + test("toBeFinite()", () => { + expect(0).toBeFinite(); + expect(1).toBeFinite(); + expect(1.23).toBeFinite(); + expect(Infinity).not.toBeFinite(); + expect(-Infinity).not.toBeFinite(); + expect(NaN).not.toBeFinite(); + expect("").not.toBeFinite(); + expect({}).not.toBeFinite(); + }); + + test("toBePositive()", () => { + expect(1).toBePositive(); + expect(1.23).toBePositive(); + expect(Infinity).not.toBePositive(); + expect(0).not.toBePositive(); + expect(-Infinity).not.toBePositive(); + expect(NaN).not.toBePositive(); + expect("").not.toBePositive(); + expect({}).not.toBePositive(); + }); + + test("toBeNegative()", () => { + expect(-1).toBeNegative(); + expect(-1.23).toBeNegative(); + expect(-Infinity).not.toBeNegative(); + expect(0).not.toBeNegative(); + expect(Infinity).not.toBeNegative(); + expect(NaN).not.toBeNegative(); + expect("").not.toBeNegative(); + expect({}).not.toBeNegative(); + }); + + test("toBeWithin()", () => { + expect(0).toBeWithin(0, 1); + expect(3.14).toBeWithin(3, 3.141); + expect(-25).toBeWithin(-100, 0); + expect(0).not.toBeWithin(1, 2); + expect(3.14).not.toBeWithin(3.1, 3.14); + expect(99).not.toBeWithin(99, 99); + expect(100).not.toBeWithin(99, 100); + expect(NaN).not.toBeWithin(0, 1); + expect("").not.toBeWithin(0, 1); + expect({}).not.toBeWithin(0, 1); + expect(Infinity).not.toBeWithin(-Infinity, Infinity); + }); + + test("toBeSymbol()", () => { + expect(Symbol()).toBeSymbol(); + expect(Symbol("")).toBeSymbol(); + expect(Symbol.iterator).toBeSymbol(); + expect("").not.toBeSymbol(); + expect({}).not.toBeSymbol(); + }); + + test("toBeFunction()", () => { + expect(() => {}).toBeFunction(); + expect(function () {}).toBeFunction(); + expect(async function () {}).toBeFunction(); + expect(async () => {}).toBeFunction(); + expect(function* () {}).toBeFunction(); + expect(async function* () {}).toBeFunction(); + expect("").not.toBeFunction(); + expect({}).not.toBeFunction(); + expect(null).not.toBeFunction(); + }); + + test("toBeDate()", () => { + expect(new Date()).toBeDate(); + expect(new Date(0)).toBeDate(); + expect(new Date("2021-01-01")).toBeDate(); + expect("2021-01-01").not.toBeDate(); + expect({}).not.toBeDate(); + expect(null).not.toBeDate(); + }); + + test.todo("toBeValidDate()", () => { + expect(new Date()).toBeValidDate(); + expect(new Date(-1)).not.toBeValidDate(); + expect("2021-01-01").not.toBeValidDate(); + expect({}).not.toBeValidDate(); + expect(null).not.toBeValidDate(); + }); + + test("toBeString()", () => { + expect("").toBeString(); + expect("123").toBeString(); + expect(new String()).toBeString(); + expect(new String("123")).toBeString(); + expect(123).not.toBeString(); + expect({}).not.toBeString(); + }); + + test("toInclude()", () => { + expect("123").toInclude("1"); + expect("abc").toInclude("abc"); + expect(" 123 ").toInclude(" "); + expect("").toInclude(""); + expect("bob").not.toInclude("alice"); + }); + + test("toStartWith()", () => { + expect("123").toStartWith("1"); + expect("abc").toStartWith("abc"); + expect(" 123 ").toStartWith(" "); + expect(" ").toStartWith(""); + expect("").toStartWith(""); + expect("bob").not.toStartWith("alice"); + }); + + test("toEndWith()", () => { + expect("123").toEndWith("3"); + expect("abc").toEndWith("abc"); + expect(" 123 ").toEndWith(" "); + expect(" ").toEndWith(""); + expect("").toEndWith(""); + expect("bob").not.toEndWith("alice"); + }); }); function label(value: unknown): string { diff --git a/test/js/bun/test/skip-test-fixture.js b/test/js/bun/test/skip-test-fixture.js new file mode 100644 index 000000000..acb5f0748 --- /dev/null +++ b/test/js/bun/test/skip-test-fixture.js @@ -0,0 +1,67 @@ +import { test, describe } from "bun:test"; + +test.skip("test #1", () => { + console.log("unreachable"); +}); + +test.skipIf(true)("test #2", () => { + console.log("unreachable"); +}); + +test.skipIf(1)("test #3", () => { + console.log("unreachable"); +}); + +test.skipIf(false)("test #4", () => { + console.log("reachable"); +}); + +test.skipIf(null)("test #5", () => { + console.log("reachable"); +}); + +describe.skip("describe #1", () => { + test("test #6", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(true)("describe #2", () => { + test("test #7", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(1)("describe #3", () => { + test("test #8", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(false)("describe #4", () => { + test("test #9", () => { + console.log("reachable"); + }); +}); + +describe.skipIf(null)("describe #5", () => { + test("test #10", () => { + console.log("reachable"); + }); +}); + +test.if(false)("test #11", () => { + console.log("unreachable"); +}); + +test.if(null)("test #12", () => { + console.log("unreachable"); +}); + +test.if(true)("test #13", () => { + console.log("reachable"); +}); + +test.if(1)("test #14", () => { + console.log("reachable"); +}); diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 4d31c9cb4..ed356aa50 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -1725,6 +1725,11 @@ test("toHaveProperty() - all", () => { expect({ a: new String("a") }).not.toHaveProperty("a", "a"); }); +test("toHaveProperty() - null or undefined", () => { + expect(null).not.toHaveProperty("length"); + expect(undefined).not.toHaveProperty("length"); +}); + test("toBe()", () => { const a = 1; const b = 1; @@ -2530,14 +2535,7 @@ describe("throw in describe scope doesn't enqueue tests after thrown", () => { throw new Error("This test failed"); }); - class TestPass extends Error { - constructor(message) { - super(message); - this.name = "TestPass"; - } - } - - throw new TestPass("This test passed. Ignore the error message"); + throw "This test passed. Ignore the error message"; it("test enqueued after a describe scope throws is never run", () => { throw new Error("This test failed"); @@ -2812,7 +2810,7 @@ it("test.todo", () => { const path = join(tmp, "todo-test.test.js"); copyFileSync(join(import.meta.dir, "todo-test-fixture.js"), path); const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "test", path], + cmd: [bunExe(), "test", path, "--todo"], stdout: "pipe", stderr: "pipe", env: bunEnv, @@ -2833,7 +2831,7 @@ it("test.todo doesnt cause exit code 1", () => { const path = join(tmp, "todo-test.test.js"); copyFileSync(join(import.meta.dir, "todo-test-fixture-2.js"), path); const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "test", path], + cmd: [bunExe(), "test", path, "--todo"], stdout: "pipe", stderr: "pipe", env: bunEnv, @@ -2919,3 +2917,19 @@ afterAll: #2 `.trim(), ); }); + +it("skip() and skipIf()", () => { + const path = join(tmp, "skip-test-fixture.test.js"); + copyFileSync(join(import.meta.dir, "skip-test-fixture.js"), path); + const { stdout } = spawnSync({ + cmd: [bunExe(), "test", path], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + cwd: realpathSync(dirname(path)), + }); + const result = stdout!.toString(); + expect(result).not.toContain("unreachable"); + expect(result).toMatch(/reachable/); + expect(result.match(/reachable/g)).toHaveLength(6); +}); diff --git a/test/js/bun/test/timeout-test-fixture.js b/test/js/bun/test/timeout-test-fixture.js index 82e887fb8..36a4ee2c6 100644 --- a/test/js/bun/test/timeout-test-fixture.js +++ b/test/js/bun/test/timeout-test-fixture.js @@ -7,6 +7,17 @@ test("test timeouts when expected", async () => { console.error("unreachable code"); }, 10); +test( + "test timeouts when expected 2", + async () => { + for (let i = 0; i < 100; i++) { + await Bun.sleep(1); + } + console.error("unreachable code"); + }, + { timeout: 10 }, +); + test("process doesn't hang on test with ref'd value", async () => { Bun.serve({ port: 0, |