@stackframe/stack-shared 2.7.20 β†’ 2.7.22

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.
@@ -21,8 +21,9 @@ export const Result = {
21
21
  return result.status === "ok" ? result.data : fallback;
22
22
  },
23
23
  orThrow: (result) => {
24
- if (result.status === "error")
24
+ if (result.status === "error") {
25
25
  throw result.error;
26
+ }
26
27
  return result.data;
27
28
  },
28
29
  orThrowAsync: async (result) => {
@@ -30,6 +31,43 @@ export const Result = {
30
31
  },
31
32
  retry,
32
33
  };
34
+ import.meta.vitest?.test("Result.ok and Result.error", ({ expect }) => {
35
+ // Test Result.ok
36
+ const okResult = Result.ok(42);
37
+ expect(okResult.status).toBe("ok");
38
+ expect(okResult.data).toBe(42);
39
+ // Test Result.error
40
+ const error = new Error("Test error");
41
+ const errorResult = Result.error(error);
42
+ expect(errorResult.status).toBe("error");
43
+ expect(errorResult.error).toBe(error);
44
+ });
45
+ import.meta.vitest?.test("Result.or", ({ expect }) => {
46
+ // Test with ok result
47
+ const okResult = { status: "ok", data: 42 };
48
+ expect(Result.or(okResult, 0)).toBe(42);
49
+ // Test with error result
50
+ const errorResult = { status: "error", error: "error message" };
51
+ expect(Result.or(errorResult, 0)).toBe(0);
52
+ });
53
+ import.meta.vitest?.test("Result.orThrow", ({ expect }) => {
54
+ // Test with ok result
55
+ const okResult = { status: "ok", data: 42 };
56
+ expect(Result.orThrow(okResult)).toBe(42);
57
+ // Test with error result
58
+ const error = new Error("Test error");
59
+ const errorResult = { status: "error", error };
60
+ expect(() => Result.orThrow(errorResult)).toThrow(error);
61
+ });
62
+ import.meta.vitest?.test("Result.orThrowAsync", async ({ expect }) => {
63
+ // Test with ok result
64
+ const okPromise = Promise.resolve({ status: "ok", data: 42 });
65
+ expect(await Result.orThrowAsync(okPromise)).toBe(42);
66
+ // Test with error result
67
+ const error = new Error("Test error");
68
+ const errorPromise = Promise.resolve({ status: "error", error });
69
+ await expect(Result.orThrowAsync(errorPromise)).rejects.toThrow(error);
70
+ });
33
71
  export const AsyncResult = {
34
72
  fromThrowing,
35
73
  fromPromise: promiseToResult,
@@ -38,23 +76,59 @@ export const AsyncResult = {
38
76
  pending,
39
77
  map: mapResult,
40
78
  or: (result, fallback) => {
41
- if (result.status === "pending")
79
+ if (result.status === "pending") {
42
80
  return fallback;
81
+ }
43
82
  return Result.or(result, fallback);
44
83
  },
45
84
  orThrow: (result) => {
46
- if (result.status === "pending")
85
+ if (result.status === "pending") {
47
86
  throw new Error("Result still pending");
87
+ }
48
88
  return Result.orThrow(result);
49
89
  },
50
90
  retry,
51
91
  };
92
+ import.meta.vitest?.test("AsyncResult.or", ({ expect }) => {
93
+ // Test with ok result
94
+ const okResult = { status: "ok", data: 42 };
95
+ expect(AsyncResult.or(okResult, 0)).toBe(42);
96
+ // Test with error result
97
+ const errorResult = { status: "error", error: "error message" };
98
+ expect(AsyncResult.or(errorResult, 0)).toBe(0);
99
+ // Test with pending result
100
+ const pendingResult = { status: "pending", progress: undefined };
101
+ expect(AsyncResult.or(pendingResult, 0)).toBe(0);
102
+ });
103
+ import.meta.vitest?.test("AsyncResult.orThrow", ({ expect }) => {
104
+ // Test with ok result
105
+ const okResult = { status: "ok", data: 42 };
106
+ expect(AsyncResult.orThrow(okResult)).toBe(42);
107
+ // Test with error result
108
+ const error = new Error("Test error");
109
+ const errorResult = { status: "error", error };
110
+ expect(() => AsyncResult.orThrow(errorResult)).toThrow(error);
111
+ // Test with pending result
112
+ const pendingResult = { status: "pending", progress: undefined };
113
+ expect(() => AsyncResult.orThrow(pendingResult)).toThrow("Result still pending");
114
+ });
52
115
  function pending(progress) {
53
116
  return {
54
117
  status: "pending",
55
118
  progress: progress,
56
119
  };
57
120
  }
121
+ import.meta.vitest?.test("pending", ({ expect }) => {
122
+ // Test without progress
123
+ const pendingResult = pending();
124
+ expect(pendingResult.status).toBe("pending");
125
+ expect(pendingResult.progress).toBe(undefined);
126
+ // Test with progress
127
+ const progressValue = { loaded: 50, total: 100 };
128
+ const pendingWithProgress = pending(progressValue);
129
+ expect(pendingWithProgress.status).toBe("pending");
130
+ expect(pendingWithProgress.progress).toBe(progressValue);
131
+ });
58
132
  async function promiseToResult(promise) {
59
133
  try {
60
134
  const value = await promise;
@@ -64,6 +138,23 @@ async function promiseToResult(promise) {
64
138
  return Result.error(error);
65
139
  }
66
140
  }
141
+ import.meta.vitest?.test("promiseToResult", async ({ expect }) => {
142
+ // Test with resolved promise
143
+ const resolvedPromise = Promise.resolve(42);
144
+ const resolvedResult = await promiseToResult(resolvedPromise);
145
+ expect(resolvedResult.status).toBe("ok");
146
+ if (resolvedResult.status === "ok") {
147
+ expect(resolvedResult.data).toBe(42);
148
+ }
149
+ // Test with rejected promise
150
+ const error = new Error("Test error");
151
+ const rejectedPromise = Promise.reject(error);
152
+ const rejectedResult = await promiseToResult(rejectedPromise);
153
+ expect(rejectedResult.status).toBe("error");
154
+ if (rejectedResult.status === "error") {
155
+ expect(rejectedResult.error).toBe(error);
156
+ }
157
+ });
67
158
  function fromThrowing(fn) {
68
159
  try {
69
160
  return Result.ok(fn());
@@ -72,6 +163,25 @@ function fromThrowing(fn) {
72
163
  return Result.error(error);
73
164
  }
74
165
  }
166
+ import.meta.vitest?.test("fromThrowing", ({ expect }) => {
167
+ // Test with function that succeeds
168
+ const successFn = () => 42;
169
+ const successResult = fromThrowing(successFn);
170
+ expect(successResult.status).toBe("ok");
171
+ if (successResult.status === "ok") {
172
+ expect(successResult.data).toBe(42);
173
+ }
174
+ // Test with function that throws
175
+ const error = new Error("Test error");
176
+ const errorFn = () => {
177
+ throw error;
178
+ };
179
+ const errorResult = fromThrowing(errorFn);
180
+ expect(errorResult.status).toBe("error");
181
+ if (errorResult.status === "error") {
182
+ expect(errorResult.error).toBe(error);
183
+ }
184
+ });
75
185
  async function fromThrowingAsync(fn) {
76
186
  try {
77
187
  return Result.ok(await fn());
@@ -80,6 +190,25 @@ async function fromThrowingAsync(fn) {
80
190
  return Result.error(error);
81
191
  }
82
192
  }
193
+ import.meta.vitest?.test("fromThrowingAsync", async ({ expect }) => {
194
+ // Test with async function that succeeds
195
+ const successFn = async () => 42;
196
+ const successResult = await fromThrowingAsync(successFn);
197
+ expect(successResult.status).toBe("ok");
198
+ if (successResult.status === "ok") {
199
+ expect(successResult.data).toBe(42);
200
+ }
201
+ // Test with async function that throws
202
+ const error = new Error("Test error");
203
+ const errorFn = async () => {
204
+ throw error;
205
+ };
206
+ const errorResult = await fromThrowingAsync(errorFn);
207
+ expect(errorResult.status).toBe("error");
208
+ if (errorResult.status === "error") {
209
+ expect(errorResult.error).toBe(error);
210
+ }
211
+ });
83
212
  function mapResult(result, fn) {
84
213
  if (result.status === "error")
85
214
  return {
@@ -93,6 +222,37 @@ function mapResult(result, fn) {
93
222
  };
94
223
  return Result.ok(fn(result.data));
95
224
  }
225
+ import.meta.vitest?.test("mapResult", ({ expect }) => {
226
+ // Test with ok result
227
+ const okResult = { status: "ok", data: 42 };
228
+ const mappedOk = mapResult(okResult, (n) => n * 2);
229
+ expect(mappedOk.status).toBe("ok");
230
+ if (mappedOk.status === "ok") {
231
+ expect(mappedOk.data).toBe(84);
232
+ }
233
+ // Test with error result
234
+ const errorResult = { status: "error", error: "error message" };
235
+ const mappedError = mapResult(errorResult, (n) => n * 2);
236
+ expect(mappedError.status).toBe("error");
237
+ if (mappedError.status === "error") {
238
+ expect(mappedError.error).toBe("error message");
239
+ }
240
+ // Test with pending result (no progress)
241
+ const pendingResult = { status: "pending", progress: undefined };
242
+ const mappedPending = mapResult(pendingResult, (n) => n * 2);
243
+ expect(mappedPending.status).toBe("pending");
244
+ // Test with pending result (with progress)
245
+ const progressValue = { loaded: 50, total: 100 };
246
+ const pendingWithProgress = {
247
+ status: "pending",
248
+ progress: progressValue
249
+ };
250
+ const mappedPendingWithProgress = mapResult(pendingWithProgress, (n) => n * 2);
251
+ expect(mappedPendingWithProgress.status).toBe("pending");
252
+ if (mappedPendingWithProgress.status === "pending") {
253
+ expect(mappedPendingWithProgress.progress).toBe(progressValue);
254
+ }
255
+ });
96
256
  class RetryError extends AggregateError {
97
257
  constructor(errors) {
98
258
  const strings = errors.map(e => String(e));
@@ -111,17 +271,47 @@ class RetryError extends AggregateError {
111
271
  this.errors = errors;
112
272
  this.name = "RetryError";
113
273
  }
114
- get retries() {
274
+ get attempts() {
115
275
  return this.errors.length;
116
276
  }
117
277
  }
118
278
  RetryError.prototype.name = "RetryError";
279
+ import.meta.vitest?.test("RetryError", ({ expect }) => {
280
+ // Test with single error
281
+ const singleError = new Error("Single error");
282
+ const retryErrorSingle = new RetryError([singleError]);
283
+ expect(retryErrorSingle.name).toBe("RetryError");
284
+ expect(retryErrorSingle.errors).toEqual([singleError]);
285
+ expect(retryErrorSingle.attempts).toBe(1);
286
+ expect(retryErrorSingle.cause).toBe(singleError);
287
+ expect(retryErrorSingle.message).toContain("Error after 1 attempts");
288
+ // Test with multiple different errors
289
+ const error1 = new Error("Error 1");
290
+ const error2 = new Error("Error 2");
291
+ const retryErrorMultiple = new RetryError([error1, error2]);
292
+ expect(retryErrorMultiple.name).toBe("RetryError");
293
+ expect(retryErrorMultiple.errors).toEqual([error1, error2]);
294
+ expect(retryErrorMultiple.attempts).toBe(2);
295
+ expect(retryErrorMultiple.cause).toBe(error2);
296
+ expect(retryErrorMultiple.message).toContain("Error after 2 attempts");
297
+ expect(retryErrorMultiple.message).toContain("Attempt 1");
298
+ expect(retryErrorMultiple.message).toContain("Attempt 2");
299
+ // Test with multiple identical errors
300
+ const sameError = new Error("Same error");
301
+ const retryErrorSame = new RetryError([sameError, sameError]);
302
+ expect(retryErrorSame.name).toBe("RetryError");
303
+ expect(retryErrorSame.errors).toEqual([sameError, sameError]);
304
+ expect(retryErrorSame.attempts).toBe(2);
305
+ expect(retryErrorSame.cause).toBe(sameError);
306
+ expect(retryErrorSame.message).toContain("Error after 2 attempts");
307
+ expect(retryErrorSame.message).toContain("Attempts 1-2");
308
+ });
119
309
  async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
120
310
  const errors = [];
121
311
  for (let i = 0; i < totalAttempts; i++) {
122
312
  const res = await fn(i);
123
313
  if (res.status === "ok") {
124
- return Result.ok(res.data);
314
+ return Object.assign(Result.ok(res.data), { attempts: i + 1 });
125
315
  }
126
316
  else {
127
317
  errors.push(res.error);
@@ -130,5 +320,29 @@ async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
130
320
  }
131
321
  }
132
322
  }
133
- return Result.error(new RetryError(errors));
323
+ return Object.assign(Result.error(new RetryError(errors)), { attempts: totalAttempts });
134
324
  }
325
+ import.meta.vitest?.test("retry", async ({ expect }) => {
326
+ // Test successful on first attempt
327
+ const successFn = async () => Result.ok("success");
328
+ const successResult = await retry(successFn, 3, { exponentialDelayBase: 0 });
329
+ expect(successResult).toEqual({ status: "ok", data: "success", attempts: 1 });
330
+ // Test successful after failures
331
+ let attemptCount = 0;
332
+ const eventualSuccessFn = async () => {
333
+ return ++attemptCount < 2 ? Result.error(new Error(`Attempt ${attemptCount} failed`))
334
+ : Result.ok("eventual success");
335
+ };
336
+ const eventualSuccessResult = await retry(eventualSuccessFn, 3, { exponentialDelayBase: 0 });
337
+ expect(eventualSuccessResult).toEqual({ status: "ok", data: "eventual success", attempts: 2 });
338
+ // Test all attempts fail
339
+ const errors = [new Error("Error 1"), new Error("Error 2"), new Error("Error 3")];
340
+ const allFailFn = async (attempt) => {
341
+ return Result.error(errors[attempt]);
342
+ };
343
+ const allFailResult = await retry(allFailFn, 3, { exponentialDelayBase: 0 });
344
+ expect(allFailResult).toEqual({ status: "error", error: expect.any(RetryError), attempts: 3 });
345
+ const retryError = allFailResult.error;
346
+ expect(retryError.errors).toEqual(errors);
347
+ expect(retryError.attempts).toBe(3);
348
+ });
@@ -4,12 +4,39 @@ import { filterUndefined } from "./objects";
4
4
  export function typedToLowercase(s) {
5
5
  return s.toLowerCase();
6
6
  }
7
+ import.meta.vitest?.test("typedToLowercase", ({ expect }) => {
8
+ expect(typedToLowercase("")).toBe("");
9
+ expect(typedToLowercase("HELLO")).toBe("hello");
10
+ expect(typedToLowercase("Hello World")).toBe("hello world");
11
+ expect(typedToLowercase("hello")).toBe("hello");
12
+ expect(typedToLowercase("123")).toBe("123");
13
+ expect(typedToLowercase("MIXED123case")).toBe("mixed123case");
14
+ expect(typedToLowercase("Special@Chars!")).toBe("special@chars!");
15
+ });
7
16
  export function typedToUppercase(s) {
8
17
  return s.toUpperCase();
9
18
  }
19
+ import.meta.vitest?.test("typedToUppercase", ({ expect }) => {
20
+ expect(typedToUppercase("")).toBe("");
21
+ expect(typedToUppercase("hello")).toBe("HELLO");
22
+ expect(typedToUppercase("Hello World")).toBe("HELLO WORLD");
23
+ expect(typedToUppercase("HELLO")).toBe("HELLO");
24
+ expect(typedToUppercase("123")).toBe("123");
25
+ expect(typedToUppercase("mixed123Case")).toBe("MIXED123CASE");
26
+ expect(typedToUppercase("special@chars!")).toBe("SPECIAL@CHARS!");
27
+ });
10
28
  export function typedCapitalize(s) {
11
29
  return s.charAt(0).toUpperCase() + s.slice(1);
12
30
  }
31
+ import.meta.vitest?.test("typedCapitalize", ({ expect }) => {
32
+ expect(typedCapitalize("")).toBe("");
33
+ expect(typedCapitalize("hello")).toBe("Hello");
34
+ expect(typedCapitalize("hello world")).toBe("Hello world");
35
+ expect(typedCapitalize("HELLO")).toBe("HELLO");
36
+ expect(typedCapitalize("123test")).toBe("123test");
37
+ expect(typedCapitalize("already Capitalized")).toBe("Already Capitalized");
38
+ expect(typedCapitalize("h")).toBe("H");
39
+ });
13
40
  /**
14
41
  * Compares two strings in a way that is not dependent on the current locale.
15
42
  */
@@ -17,6 +44,29 @@ export function stringCompare(a, b) {
17
44
  const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
18
45
  return cmp(a.toUpperCase(), b.toUpperCase()) || cmp(b, a);
19
46
  }
47
+ import.meta.vitest?.test("stringCompare", ({ expect }) => {
48
+ // Equal strings
49
+ expect(stringCompare("a", "a")).toBe(0);
50
+ expect(stringCompare("", "")).toBe(0);
51
+ // Case comparison - note that this function is NOT case-insensitive
52
+ // It compares uppercase versions first, then original strings
53
+ expect(stringCompare("a", "A")).toBe(-1); // lowercase comes after uppercase
54
+ expect(stringCompare("A", "a")).toBe(1); // uppercase comes before lowercase
55
+ expect(stringCompare("abc", "ABC")).toBe(-1);
56
+ expect(stringCompare("ABC", "abc")).toBe(1);
57
+ // Different strings
58
+ expect(stringCompare("a", "b")).toBe(-1);
59
+ expect(stringCompare("b", "a")).toBe(1);
60
+ // Strings with different lengths
61
+ expect(stringCompare("abc", "abcd")).toBe(-1);
62
+ expect(stringCompare("abcd", "abc")).toBe(1);
63
+ // Strings with numbers
64
+ expect(stringCompare("a1", "a2")).toBe(-1);
65
+ expect(stringCompare("a10", "a2")).toBe(-1);
66
+ // Strings with special characters
67
+ expect(stringCompare("a", "a!")).toBe(-1);
68
+ expect(stringCompare("a!", "a")).toBe(1);
69
+ });
20
70
  /**
21
71
  * Returns all whitespace character at the start of the string.
22
72
  *
@@ -25,6 +75,16 @@ export function stringCompare(a, b) {
25
75
  export function getWhitespacePrefix(s) {
26
76
  return s.substring(0, s.length - s.trimStart().length);
27
77
  }
78
+ import.meta.vitest?.test("getWhitespacePrefix", ({ expect }) => {
79
+ expect(getWhitespacePrefix("")).toBe("");
80
+ expect(getWhitespacePrefix("hello")).toBe("");
81
+ expect(getWhitespacePrefix(" hello")).toBe(" ");
82
+ expect(getWhitespacePrefix(" hello")).toBe(" ");
83
+ expect(getWhitespacePrefix("\thello")).toBe("\t");
84
+ expect(getWhitespacePrefix("\n hello")).toBe("\n ");
85
+ expect(getWhitespacePrefix(" ")).toBe(" ");
86
+ expect(getWhitespacePrefix(" \t\n\r")).toBe(" \t\n\r");
87
+ });
28
88
  /**
29
89
  * Returns all whitespace character at the end of the string.
30
90
  *
@@ -33,6 +93,16 @@ export function getWhitespacePrefix(s) {
33
93
  export function getWhitespaceSuffix(s) {
34
94
  return s.substring(s.trimEnd().length);
35
95
  }
96
+ import.meta.vitest?.test("getWhitespaceSuffix", ({ expect }) => {
97
+ expect(getWhitespaceSuffix("")).toBe("");
98
+ expect(getWhitespaceSuffix("hello")).toBe("");
99
+ expect(getWhitespaceSuffix("hello ")).toBe(" ");
100
+ expect(getWhitespaceSuffix("hello ")).toBe(" ");
101
+ expect(getWhitespaceSuffix("hello\t")).toBe("\t");
102
+ expect(getWhitespaceSuffix("hello \n")).toBe(" \n");
103
+ expect(getWhitespaceSuffix(" ")).toBe(" ");
104
+ expect(getWhitespaceSuffix(" \t\n\r")).toBe(" \t\n\r");
105
+ });
36
106
  /**
37
107
  * Returns a string with all empty or whitespace-only lines at the start removed.
38
108
  *
@@ -41,8 +111,24 @@ export function getWhitespaceSuffix(s) {
41
111
  export function trimEmptyLinesStart(s) {
42
112
  const lines = s.split("\n");
43
113
  const firstNonEmptyLineIndex = lines.findIndex((line) => line.trim() !== "");
114
+ // If all lines are empty or whitespace-only, return an empty string
115
+ if (firstNonEmptyLineIndex === -1)
116
+ return "";
44
117
  return lines.slice(firstNonEmptyLineIndex).join("\n");
45
118
  }
119
+ import.meta.vitest?.test("trimEmptyLinesStart", ({ expect }) => {
120
+ expect(trimEmptyLinesStart("")).toBe("");
121
+ expect(trimEmptyLinesStart("hello")).toBe("hello");
122
+ expect(trimEmptyLinesStart("\nhello")).toBe("hello");
123
+ expect(trimEmptyLinesStart("\n\nhello")).toBe("hello");
124
+ expect(trimEmptyLinesStart(" \n\t\nhello")).toBe("hello");
125
+ expect(trimEmptyLinesStart("\n\nhello\nworld")).toBe("hello\nworld");
126
+ expect(trimEmptyLinesStart("hello\n\nworld")).toBe("hello\n\nworld");
127
+ expect(trimEmptyLinesStart("hello\nworld\n")).toBe("hello\nworld\n");
128
+ expect(trimEmptyLinesStart("\n \n\nhello\n \nworld")).toBe("hello\n \nworld");
129
+ // Edge case: all lines are empty
130
+ expect(trimEmptyLinesStart("\n\n \n\t")).toBe("");
131
+ });
46
132
  /**
47
133
  * Returns a string with all empty or whitespace-only lines at the end removed.
48
134
  *
@@ -53,6 +139,19 @@ export function trimEmptyLinesEnd(s) {
53
139
  const lastNonEmptyLineIndex = findLastIndex(lines, (line) => line.trim() !== "");
54
140
  return lines.slice(0, lastNonEmptyLineIndex + 1).join("\n");
55
141
  }
142
+ import.meta.vitest?.test("trimEmptyLinesEnd", ({ expect }) => {
143
+ expect(trimEmptyLinesEnd("")).toBe("");
144
+ expect(trimEmptyLinesEnd("hello")).toBe("hello");
145
+ expect(trimEmptyLinesEnd("hello\n")).toBe("hello");
146
+ expect(trimEmptyLinesEnd("hello\n\n")).toBe("hello");
147
+ expect(trimEmptyLinesEnd("hello\n \n\t")).toBe("hello");
148
+ expect(trimEmptyLinesEnd("hello\nworld\n\n")).toBe("hello\nworld");
149
+ expect(trimEmptyLinesEnd("hello\n\nworld")).toBe("hello\n\nworld");
150
+ expect(trimEmptyLinesEnd("\nhello\nworld")).toBe("\nhello\nworld");
151
+ expect(trimEmptyLinesEnd("hello\n \nworld\n\n ")).toBe("hello\n \nworld");
152
+ // Edge case: all lines are empty
153
+ expect(trimEmptyLinesEnd("\n\n \n\t")).toBe("");
154
+ });
56
155
  /**
57
156
  * Returns a string with all empty or whitespace-only lines trimmed at the start and end.
58
157
  *
@@ -107,8 +206,8 @@ export function deindent(strings, ...values) {
107
206
  if (values.length !== strings.length - 1)
108
207
  throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
109
208
  const trimmedStrings = [...strings];
110
- trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0]);
111
- trimmedStrings[trimmedStrings.length - 1] = trimEmptyLinesEnd(trimmedStrings[trimmedStrings.length - 1]);
209
+ trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0] + "+").slice(0, -1);
210
+ trimmedStrings[trimmedStrings.length - 1] = trimEmptyLinesEnd("+" + trimmedStrings[trimmedStrings.length - 1]).slice(1);
112
211
  const indentation = trimmedStrings
113
212
  .join("${SOME_VALUE}")
114
213
  .split("\n")
@@ -128,6 +227,57 @@ export function deindent(strings, ...values) {
128
227
  });
129
228
  return templateIdentity(deindentedStrings, ...indentedValues);
130
229
  }
230
+ import.meta.vitest?.test("deindent", ({ expect }) => {
231
+ // Test with string input
232
+ expect(deindent(" hello")).toBe("hello");
233
+ expect(deindent(" hello\n world")).toBe("hello\nworld");
234
+ expect(deindent(" hello\n world")).toBe("hello\n world");
235
+ expect(deindent("\n hello\n world\n")).toBe("hello\nworld");
236
+ // Test with empty input
237
+ expect(deindent("")).toBe("");
238
+ expect(deindent([])).toBe("");
239
+ // Test with template literal
240
+ expect(deindent `
241
+ hello
242
+ world
243
+ `).toBe("hello\nworld");
244
+ expect(deindent `
245
+ hello
246
+ world
247
+ `).toBe("hello\n world");
248
+ // Test with values
249
+ const value = "test";
250
+ expect(deindent `
251
+ hello ${value}
252
+ world
253
+ `).toBe(`hello ${value}\nworld`);
254
+ // Test with multiline values
255
+ expect(deindent `
256
+ hello
257
+ to ${"line1\n line2"}
258
+ world
259
+ `).toBe(`hello\n to line1\n line2\nworld`);
260
+ // Leading whitespace values
261
+ expect(deindent `
262
+ ${" "}A
263
+ ${" "}B
264
+ ${" "}C
265
+ `).toBe(` A\n B\n C`);
266
+ // Trailing whitespaces (note: there are two whitespaces each after A and after C)
267
+ expect(deindent `
268
+ A
269
+ B ${" "}
270
+ C
271
+ `).toBe(`A \nB \nC `);
272
+ // Test with mixed indentation
273
+ expect(deindent `
274
+ hello
275
+ world
276
+ !
277
+ `).toBe("hello\n world\n !");
278
+ // Test error cases
279
+ expect(() => deindent(["a", "b", "c"], "too", "many", "values")).toThrow("Invalid number of values");
280
+ });
131
281
  export function extractScopes(scope, removeDuplicates = true) {
132
282
  // TODO what is this for? can we move this into the OAuth code in the backend?
133
283
  const trimmedString = scope.trim();
@@ -135,14 +285,85 @@ export function extractScopes(scope, removeDuplicates = true) {
135
285
  const filtered = scopesArray.filter(scope => scope.length > 0);
136
286
  return removeDuplicates ? [...new Set(filtered)] : filtered;
137
287
  }
288
+ import.meta.vitest?.test("extractScopes", ({ expect }) => {
289
+ // Test with empty string
290
+ expect(extractScopes("")).toEqual([]);
291
+ // Test with single scope
292
+ expect(extractScopes("read")).toEqual(["read"]);
293
+ // Test with multiple scopes
294
+ expect(extractScopes("read write")).toEqual(["read", "write"]);
295
+ // Test with extra whitespace
296
+ expect(extractScopes(" read write ")).toEqual(["read", "write"]);
297
+ // Test with newlines and tabs
298
+ expect(extractScopes("read\nwrite\tdelete")).toEqual(["read", "write", "delete"]);
299
+ // Test with duplicates (default behavior)
300
+ expect(extractScopes("read write read")).toEqual(["read", "write"]);
301
+ // Test with duplicates (explicitly set to remove)
302
+ expect(extractScopes("read write read", true)).toEqual(["read", "write"]);
303
+ // Test with duplicates (explicitly set to keep)
304
+ expect(extractScopes("read write read", false)).toEqual(["read", "write", "read"]);
305
+ });
138
306
  export function mergeScopeStrings(...scopes) {
139
307
  // TODO what is this for? can we move this into the OAuth code in the backend?
140
308
  const allScope = scopes.map((s) => extractScopes(s)).flat().join(" ");
141
309
  return extractScopes(allScope).join(" ");
142
310
  }
311
+ import.meta.vitest?.test("mergeScopeStrings", ({ expect }) => {
312
+ // Test with empty input
313
+ expect(mergeScopeStrings()).toBe("");
314
+ // Test with single scope string
315
+ expect(mergeScopeStrings("read write")).toBe("read write");
316
+ // Test with multiple scope strings
317
+ expect(mergeScopeStrings("read", "write")).toBe("read write");
318
+ // Test with overlapping scopes
319
+ expect(mergeScopeStrings("read write", "write delete")).toBe("read write delete");
320
+ // Test with extra whitespace
321
+ expect(mergeScopeStrings(" read write ", " delete ")).toBe("read write delete");
322
+ // Test with duplicates across strings
323
+ expect(mergeScopeStrings("read write", "write delete", "read")).toBe("read write delete");
324
+ // Test with empty strings
325
+ expect(mergeScopeStrings("read write", "", "delete")).toBe("read write delete");
326
+ });
143
327
  export function escapeTemplateLiteral(s) {
144
328
  return s.replaceAll("`", "\\`").replaceAll("\\", "\\\\").replaceAll("$", "\\$");
145
329
  }
330
+ import.meta.vitest?.test("escapeTemplateLiteral", ({ expect }) => {
331
+ // Test with empty string
332
+ expect(escapeTemplateLiteral("")).toBe("");
333
+ // Test with normal string (no special characters)
334
+ expect(escapeTemplateLiteral("hello world")).toBe("hello world");
335
+ // Test with backtick
336
+ const input1 = "hello `world`";
337
+ const output1 = escapeTemplateLiteral(input1);
338
+ // Verify backticks are escaped
339
+ expect(output1.includes("\\`")).toBe(true);
340
+ expect(output1).not.toBe(input1);
341
+ // Test with backslash
342
+ const input2 = "hello \\world";
343
+ const output2 = escapeTemplateLiteral(input2);
344
+ // Verify backslashes are escaped
345
+ expect(output2.includes("\\\\")).toBe(true);
346
+ expect(output2).not.toBe(input2);
347
+ // Test with dollar sign
348
+ const input3 = "hello $world";
349
+ const output3 = escapeTemplateLiteral(input3);
350
+ // Verify dollar signs are escaped
351
+ expect(output3.includes("\\$")).toBe(true);
352
+ expect(output3).not.toBe(input3);
353
+ // Test with multiple special characters
354
+ const input4 = "`hello` $world\\";
355
+ const output4 = escapeTemplateLiteral(input4);
356
+ // Verify all special characters are escaped
357
+ expect(output4.includes("\\`")).toBe(true);
358
+ expect(output4.includes("\\$")).toBe(true);
359
+ expect(output4.includes("\\\\")).toBe(true);
360
+ expect(output4).not.toBe(input4);
361
+ // Test with already escaped characters
362
+ const input5 = "\\`hello\\`";
363
+ const output5 = escapeTemplateLiteral(input5);
364
+ // Verify already escaped characters are properly escaped
365
+ expect(output5).not.toBe(input5);
366
+ });
146
367
  /**
147
368
  * Some classes have different constructor names in different environments (eg. `Headers` is sometimes called `_Headers`,
148
369
  * so we create an object of overrides to handle these cases.
@@ -8,3 +8,16 @@ export function getFlagEmoji(twoLetterCountryCode) {
8
8
  .map(char => 127397 + char.charCodeAt(0));
9
9
  return String.fromCodePoint(...codePoints);
10
10
  }
11
+ import.meta.vitest?.test("getFlagEmoji", ({ expect }) => {
12
+ // Test with valid country codes
13
+ expect(getFlagEmoji("US")).toBe("πŸ‡ΊπŸ‡Έ");
14
+ expect(getFlagEmoji("us")).toBe("πŸ‡ΊπŸ‡Έ");
15
+ expect(getFlagEmoji("GB")).toBe("πŸ‡¬πŸ‡§");
16
+ expect(getFlagEmoji("JP")).toBe("πŸ‡―πŸ‡΅");
17
+ // Test with invalid country codes
18
+ expect(() => getFlagEmoji("")).toThrow("Country code must be two alphabetical letters");
19
+ expect(() => getFlagEmoji("A")).toThrow("Country code must be two alphabetical letters");
20
+ expect(() => getFlagEmoji("ABC")).toThrow("Country code must be two alphabetical letters");
21
+ expect(() => getFlagEmoji("12")).toThrow("Country code must be two alphabetical letters");
22
+ expect(() => getFlagEmoji("A1")).toThrow("Country code must be two alphabetical letters");
23
+ });