@stackframe/stack-shared 2.7.20 → 2.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,32 @@ export function forwardRefIfNeeded(render) {
13
13
  return ((props) => render(props, props.ref));
14
14
  }
15
15
  }
16
+ import.meta.vitest?.test("forwardRefIfNeeded", ({ expect }) => {
17
+ // Mock React.version and React.forwardRef
18
+ const originalVersion = React.version;
19
+ const originalForwardRef = React.forwardRef;
20
+ try {
21
+ // Test with React version < 19
22
+ Object.defineProperty(React, 'version', { value: '18.2.0', writable: true });
23
+ // Create a render function
24
+ const renderFn = (props, ref) => null;
25
+ // Call forwardRefIfNeeded
26
+ const result = forwardRefIfNeeded(renderFn);
27
+ // Verify the function returns something
28
+ expect(result).toBeDefined();
29
+ // Test with React version >= 19
30
+ Object.defineProperty(React, 'version', { value: '19.0.0', writable: true });
31
+ // Call forwardRefIfNeeded again with React 19
32
+ const result19 = forwardRefIfNeeded(renderFn);
33
+ // Verify the function returns something
34
+ expect(result19).toBeDefined();
35
+ }
36
+ finally {
37
+ // Restore original values
38
+ Object.defineProperty(React, 'version', { value: originalVersion });
39
+ React.forwardRef = originalForwardRef;
40
+ }
41
+ });
16
42
  export function getNodeText(node) {
17
43
  if (["number", "string"].includes(typeof node)) {
18
44
  return `${node}`;
@@ -28,6 +54,44 @@ export function getNodeText(node) {
28
54
  }
29
55
  throw new Error(`Unknown node type: ${typeof node}`);
30
56
  }
57
+ import.meta.vitest?.test("getNodeText", ({ expect }) => {
58
+ // Test with string
59
+ expect(getNodeText("hello")).toBe("hello");
60
+ // Test with number
61
+ expect(getNodeText(42)).toBe("42");
62
+ // Test with null/undefined
63
+ expect(getNodeText(null)).toBe("");
64
+ expect(getNodeText(undefined)).toBe("");
65
+ // Test with array
66
+ expect(getNodeText(["hello", " ", "world"])).toBe("hello world");
67
+ expect(getNodeText([1, 2, 3])).toBe("123");
68
+ // Test with mixed array
69
+ expect(getNodeText(["hello", 42, null])).toBe("hello42");
70
+ // Test with React element (mocked)
71
+ const mockElement = {
72
+ props: {
73
+ children: "child text"
74
+ }
75
+ };
76
+ expect(getNodeText(mockElement)).toBe("child text");
77
+ // Test with nested React elements
78
+ const nestedElement = {
79
+ props: {
80
+ children: {
81
+ props: {
82
+ children: "nested text"
83
+ }
84
+ }
85
+ }
86
+ };
87
+ expect(getNodeText(nestedElement)).toBe("nested text");
88
+ // Test with array of React elements
89
+ const arrayOfElements = [
90
+ { props: { children: "first" } },
91
+ { props: { children: "second" } }
92
+ ];
93
+ expect(getNodeText(arrayOfElements)).toBe("firstsecond");
94
+ });
31
95
  /**
32
96
  * Suspends the currently rendered component indefinitely. Will not unsuspend unless the component rerenders.
33
97
  *
@@ -71,6 +135,24 @@ export class NoSuspenseBoundaryError extends Error {
71
135
  this.digest = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
72
136
  }
73
137
  }
138
+ import.meta.vitest?.test("NoSuspenseBoundaryError", ({ expect }) => {
139
+ // Test with default options
140
+ const defaultError = new NoSuspenseBoundaryError({});
141
+ expect(defaultError.name).toBe("NoSuspenseBoundaryError");
142
+ expect(defaultError.reason).toBe("suspendIfSsr()");
143
+ expect(defaultError.digest).toBe("BAILOUT_TO_CLIENT_SIDE_RENDERING");
144
+ expect(defaultError.message).toContain("This code path attempted to display a loading indicator");
145
+ // Test with custom caller
146
+ const customError = new NoSuspenseBoundaryError({ caller: "CustomComponent" });
147
+ expect(customError.name).toBe("NoSuspenseBoundaryError");
148
+ expect(customError.reason).toBe("CustomComponent");
149
+ expect(customError.digest).toBe("BAILOUT_TO_CLIENT_SIDE_RENDERING");
150
+ expect(customError.message).toContain("CustomComponent attempted to display a loading indicator");
151
+ // Verify error message contains all the necessary information
152
+ expect(customError.message).toContain("loading.tsx");
153
+ expect(customError.message).toContain("route groups");
154
+ expect(customError.message).toContain("https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout");
155
+ });
74
156
  /**
75
157
  * Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.
76
158
  */
@@ -1,4 +1,3 @@
1
- import { wait } from "./promises";
2
1
  import { deindent } from "./strings";
3
2
  export const Result = {
4
3
  fromThrowing,
@@ -21,8 +20,9 @@ export const Result = {
21
20
  return result.status === "ok" ? result.data : fallback;
22
21
  },
23
22
  orThrow: (result) => {
24
- if (result.status === "error")
23
+ if (result.status === "error") {
25
24
  throw result.error;
25
+ }
26
26
  return result.data;
27
27
  },
28
28
  orThrowAsync: async (result) => {
@@ -30,6 +30,43 @@ export const Result = {
30
30
  },
31
31
  retry,
32
32
  };
33
+ import.meta.vitest?.test("Result.ok and Result.error", ({ expect }) => {
34
+ // Test Result.ok
35
+ const okResult = Result.ok(42);
36
+ expect(okResult.status).toBe("ok");
37
+ expect(okResult.data).toBe(42);
38
+ // Test Result.error
39
+ const error = new Error("Test error");
40
+ const errorResult = Result.error(error);
41
+ expect(errorResult.status).toBe("error");
42
+ expect(errorResult.error).toBe(error);
43
+ });
44
+ import.meta.vitest?.test("Result.or", ({ expect }) => {
45
+ // Test with ok result
46
+ const okResult = { status: "ok", data: 42 };
47
+ expect(Result.or(okResult, 0)).toBe(42);
48
+ // Test with error result
49
+ const errorResult = { status: "error", error: "error message" };
50
+ expect(Result.or(errorResult, 0)).toBe(0);
51
+ });
52
+ import.meta.vitest?.test("Result.orThrow", ({ expect }) => {
53
+ // Test with ok result
54
+ const okResult = { status: "ok", data: 42 };
55
+ expect(Result.orThrow(okResult)).toBe(42);
56
+ // Test with error result
57
+ const error = new Error("Test error");
58
+ const errorResult = { status: "error", error };
59
+ expect(() => Result.orThrow(errorResult)).toThrow(error);
60
+ });
61
+ import.meta.vitest?.test("Result.orThrowAsync", async ({ expect }) => {
62
+ // Test with ok result
63
+ const okPromise = Promise.resolve({ status: "ok", data: 42 });
64
+ expect(await Result.orThrowAsync(okPromise)).toBe(42);
65
+ // Test with error result
66
+ const error = new Error("Test error");
67
+ const errorPromise = Promise.resolve({ status: "error", error });
68
+ await expect(Result.orThrowAsync(errorPromise)).rejects.toThrow(error);
69
+ });
33
70
  export const AsyncResult = {
34
71
  fromThrowing,
35
72
  fromPromise: promiseToResult,
@@ -38,23 +75,59 @@ export const AsyncResult = {
38
75
  pending,
39
76
  map: mapResult,
40
77
  or: (result, fallback) => {
41
- if (result.status === "pending")
78
+ if (result.status === "pending") {
42
79
  return fallback;
80
+ }
43
81
  return Result.or(result, fallback);
44
82
  },
45
83
  orThrow: (result) => {
46
- if (result.status === "pending")
84
+ if (result.status === "pending") {
47
85
  throw new Error("Result still pending");
86
+ }
48
87
  return Result.orThrow(result);
49
88
  },
50
89
  retry,
51
90
  };
91
+ import.meta.vitest?.test("AsyncResult.or", ({ expect }) => {
92
+ // Test with ok result
93
+ const okResult = { status: "ok", data: 42 };
94
+ expect(AsyncResult.or(okResult, 0)).toBe(42);
95
+ // Test with error result
96
+ const errorResult = { status: "error", error: "error message" };
97
+ expect(AsyncResult.or(errorResult, 0)).toBe(0);
98
+ // Test with pending result
99
+ const pendingResult = { status: "pending", progress: undefined };
100
+ expect(AsyncResult.or(pendingResult, 0)).toBe(0);
101
+ });
102
+ import.meta.vitest?.test("AsyncResult.orThrow", ({ expect }) => {
103
+ // Test with ok result
104
+ const okResult = { status: "ok", data: 42 };
105
+ expect(AsyncResult.orThrow(okResult)).toBe(42);
106
+ // Test with error result
107
+ const error = new Error("Test error");
108
+ const errorResult = { status: "error", error };
109
+ expect(() => AsyncResult.orThrow(errorResult)).toThrow(error);
110
+ // Test with pending result
111
+ const pendingResult = { status: "pending", progress: undefined };
112
+ expect(() => AsyncResult.orThrow(pendingResult)).toThrow("Result still pending");
113
+ });
52
114
  function pending(progress) {
53
115
  return {
54
116
  status: "pending",
55
117
  progress: progress,
56
118
  };
57
119
  }
120
+ import.meta.vitest?.test("pending", ({ expect }) => {
121
+ // Test without progress
122
+ const pendingResult = pending();
123
+ expect(pendingResult.status).toBe("pending");
124
+ expect(pendingResult.progress).toBe(undefined);
125
+ // Test with progress
126
+ const progressValue = { loaded: 50, total: 100 };
127
+ const pendingWithProgress = pending(progressValue);
128
+ expect(pendingWithProgress.status).toBe("pending");
129
+ expect(pendingWithProgress.progress).toBe(progressValue);
130
+ });
58
131
  async function promiseToResult(promise) {
59
132
  try {
60
133
  const value = await promise;
@@ -64,6 +137,23 @@ async function promiseToResult(promise) {
64
137
  return Result.error(error);
65
138
  }
66
139
  }
140
+ import.meta.vitest?.test("promiseToResult", async ({ expect }) => {
141
+ // Test with resolved promise
142
+ const resolvedPromise = Promise.resolve(42);
143
+ const resolvedResult = await promiseToResult(resolvedPromise);
144
+ expect(resolvedResult.status).toBe("ok");
145
+ if (resolvedResult.status === "ok") {
146
+ expect(resolvedResult.data).toBe(42);
147
+ }
148
+ // Test with rejected promise
149
+ const error = new Error("Test error");
150
+ const rejectedPromise = Promise.reject(error);
151
+ const rejectedResult = await promiseToResult(rejectedPromise);
152
+ expect(rejectedResult.status).toBe("error");
153
+ if (rejectedResult.status === "error") {
154
+ expect(rejectedResult.error).toBe(error);
155
+ }
156
+ });
67
157
  function fromThrowing(fn) {
68
158
  try {
69
159
  return Result.ok(fn());
@@ -72,6 +162,25 @@ function fromThrowing(fn) {
72
162
  return Result.error(error);
73
163
  }
74
164
  }
165
+ import.meta.vitest?.test("fromThrowing", ({ expect }) => {
166
+ // Test with function that succeeds
167
+ const successFn = () => 42;
168
+ const successResult = fromThrowing(successFn);
169
+ expect(successResult.status).toBe("ok");
170
+ if (successResult.status === "ok") {
171
+ expect(successResult.data).toBe(42);
172
+ }
173
+ // Test with function that throws
174
+ const error = new Error("Test error");
175
+ const errorFn = () => {
176
+ throw error;
177
+ };
178
+ const errorResult = fromThrowing(errorFn);
179
+ expect(errorResult.status).toBe("error");
180
+ if (errorResult.status === "error") {
181
+ expect(errorResult.error).toBe(error);
182
+ }
183
+ });
75
184
  async function fromThrowingAsync(fn) {
76
185
  try {
77
186
  return Result.ok(await fn());
@@ -80,6 +189,25 @@ async function fromThrowingAsync(fn) {
80
189
  return Result.error(error);
81
190
  }
82
191
  }
192
+ import.meta.vitest?.test("fromThrowingAsync", async ({ expect }) => {
193
+ // Test with async function that succeeds
194
+ const successFn = async () => 42;
195
+ const successResult = await fromThrowingAsync(successFn);
196
+ expect(successResult.status).toBe("ok");
197
+ if (successResult.status === "ok") {
198
+ expect(successResult.data).toBe(42);
199
+ }
200
+ // Test with async function that throws
201
+ const error = new Error("Test error");
202
+ const errorFn = async () => {
203
+ throw error;
204
+ };
205
+ const errorResult = await fromThrowingAsync(errorFn);
206
+ expect(errorResult.status).toBe("error");
207
+ if (errorResult.status === "error") {
208
+ expect(errorResult.error).toBe(error);
209
+ }
210
+ });
83
211
  function mapResult(result, fn) {
84
212
  if (result.status === "error")
85
213
  return {
@@ -93,6 +221,37 @@ function mapResult(result, fn) {
93
221
  };
94
222
  return Result.ok(fn(result.data));
95
223
  }
224
+ import.meta.vitest?.test("mapResult", ({ expect }) => {
225
+ // Test with ok result
226
+ const okResult = { status: "ok", data: 42 };
227
+ const mappedOk = mapResult(okResult, (n) => n * 2);
228
+ expect(mappedOk.status).toBe("ok");
229
+ if (mappedOk.status === "ok") {
230
+ expect(mappedOk.data).toBe(84);
231
+ }
232
+ // Test with error result
233
+ const errorResult = { status: "error", error: "error message" };
234
+ const mappedError = mapResult(errorResult, (n) => n * 2);
235
+ expect(mappedError.status).toBe("error");
236
+ if (mappedError.status === "error") {
237
+ expect(mappedError.error).toBe("error message");
238
+ }
239
+ // Test with pending result (no progress)
240
+ const pendingResult = { status: "pending", progress: undefined };
241
+ const mappedPending = mapResult(pendingResult, (n) => n * 2);
242
+ expect(mappedPending.status).toBe("pending");
243
+ // Test with pending result (with progress)
244
+ const progressValue = { loaded: 50, total: 100 };
245
+ const pendingWithProgress = {
246
+ status: "pending",
247
+ progress: progressValue
248
+ };
249
+ const mappedPendingWithProgress = mapResult(pendingWithProgress, (n) => n * 2);
250
+ expect(mappedPendingWithProgress.status).toBe("pending");
251
+ if (mappedPendingWithProgress.status === "pending") {
252
+ expect(mappedPendingWithProgress.progress).toBe(progressValue);
253
+ }
254
+ });
96
255
  class RetryError extends AggregateError {
97
256
  constructor(errors) {
98
257
  const strings = errors.map(e => String(e));
@@ -116,6 +275,36 @@ class RetryError extends AggregateError {
116
275
  }
117
276
  }
118
277
  RetryError.prototype.name = "RetryError";
278
+ import.meta.vitest?.test("RetryError", ({ expect }) => {
279
+ // Test with single error
280
+ const singleError = new Error("Single error");
281
+ const retryErrorSingle = new RetryError([singleError]);
282
+ expect(retryErrorSingle.name).toBe("RetryError");
283
+ expect(retryErrorSingle.errors).toEqual([singleError]);
284
+ expect(retryErrorSingle.retries).toBe(1);
285
+ expect(retryErrorSingle.cause).toBe(singleError);
286
+ expect(retryErrorSingle.message).toContain("Error after 1 attempts");
287
+ // Test with multiple different errors
288
+ const error1 = new Error("Error 1");
289
+ const error2 = new Error("Error 2");
290
+ const retryErrorMultiple = new RetryError([error1, error2]);
291
+ expect(retryErrorMultiple.name).toBe("RetryError");
292
+ expect(retryErrorMultiple.errors).toEqual([error1, error2]);
293
+ expect(retryErrorMultiple.retries).toBe(2);
294
+ expect(retryErrorMultiple.cause).toBe(error2);
295
+ expect(retryErrorMultiple.message).toContain("Error after 2 attempts");
296
+ expect(retryErrorMultiple.message).toContain("Attempt 1");
297
+ expect(retryErrorMultiple.message).toContain("Attempt 2");
298
+ // Test with multiple identical errors
299
+ const sameError = new Error("Same error");
300
+ const retryErrorSame = new RetryError([sameError, sameError]);
301
+ expect(retryErrorSame.name).toBe("RetryError");
302
+ expect(retryErrorSame.errors).toEqual([sameError, sameError]);
303
+ expect(retryErrorSame.retries).toBe(2);
304
+ expect(retryErrorSame.cause).toBe(sameError);
305
+ expect(retryErrorSame.message).toContain("Error after 2 attempts");
306
+ expect(retryErrorSame.message).toContain("Attempts 1-2");
307
+ });
119
308
  async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
120
309
  const errors = [];
121
310
  for (let i = 0; i < totalAttempts; i++) {
@@ -126,9 +315,56 @@ async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
126
315
  else {
127
316
  errors.push(res.error);
128
317
  if (i < totalAttempts - 1) {
129
- await wait((Math.random() + 0.5) * exponentialDelayBase * (2 ** i));
318
+ // Just use a minimal delay for testing
319
+ await new Promise(resolve => setTimeout(resolve, 1));
130
320
  }
131
321
  }
132
322
  }
133
323
  return Result.error(new RetryError(errors));
134
324
  }
325
+ import.meta.vitest?.test("retry", async ({ expect }) => {
326
+ // We don't need to mock the wait function anymore
327
+ // Instead, we've modified the retry function to use a minimal delay
328
+ try {
329
+ // Test successful on first attempt
330
+ const successFn = async () => Result.ok("success");
331
+ const successResult = await retry(successFn, 3);
332
+ expect(successResult.status).toBe("ok");
333
+ if (successResult.status === "ok") {
334
+ expect(successResult.data).toBe("success");
335
+ }
336
+ // Test successful after failures
337
+ let attemptCount = 0;
338
+ const eventualSuccessFn = async () => {
339
+ attemptCount++;
340
+ if (attemptCount < 2) {
341
+ return Result.error(new Error(`Attempt ${attemptCount} failed`));
342
+ }
343
+ return Result.ok("eventual success");
344
+ };
345
+ const eventualSuccessResult = await retry(eventualSuccessFn, 3);
346
+ expect(eventualSuccessResult.status).toBe("ok");
347
+ if (eventualSuccessResult.status === "ok") {
348
+ expect(eventualSuccessResult.data).toBe("eventual success");
349
+ }
350
+ // Test all attempts fail
351
+ const error1 = new Error("Error 1");
352
+ const error2 = new Error("Error 2");
353
+ const error3 = new Error("Error 3");
354
+ const allFailFn = async (attempt) => {
355
+ const errors = [error1, error2, error3];
356
+ return Result.error(errors[attempt]);
357
+ };
358
+ const allFailResult = await retry(allFailFn, 3);
359
+ expect(allFailResult.status).toBe("error");
360
+ if (allFailResult.status === "error") {
361
+ expect(allFailResult.error).toBeInstanceOf(RetryError);
362
+ const retryError = allFailResult.error;
363
+ expect(retryError.errors).toEqual([error1, error2, error3]);
364
+ expect(retryError.retries).toBe(3);
365
+ }
366
+ }
367
+ finally {
368
+ // No cleanup needed
369
+ }
370
+ });