diff --git a/scripts/smoke-dist.mjs b/scripts/smoke-dist.mjs index 82c547b..ed3da15 100644 --- a/scripts/smoke-dist.mjs +++ b/scripts/smoke-dist.mjs @@ -79,6 +79,16 @@ for (const [name, mod] of results) { expected, `${name}: parse`, ) + // Minifiers rename classes, so `name` must survive in every build. + for (const errorName of expectedExports.filter(e => e.endsWith('Error'))) { + const error = new mod[errorName]('k') + assert.equal(error.name, errorName, `${name}: ${errorName} name`) + assert.match( + String(error), + new RegExp(`^${errorName}: `), + `${name}: ${errorName} toString`, + ) + } assert.doesNotMatch( readFileSync(new URL(`../${files[name]}`, import.meta.url), 'utf8'), loadsRemixFile, diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index ebe3198..8e282c1 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -442,8 +442,65 @@ describe('errors', () => { formData.append('foo[]', 'test2') expect(() => parseFormData(formData)).toThrow(new MixedArrayError('foo[]')) }) + + it.each([ + { + name: 'DuplicateKeyError', + ErrorClass: DuplicateKeyError, + entries: [ + ['a', '1'], + ['a', '2'], + ], + depth: Infinity, + }, + { + name: 'MixedArrayError', + ErrorClass: MixedArrayError, + entries: [ + ['a[]', '1'], + ['a[0]', '2'], + ], + depth: Infinity, + }, + { + name: 'ForbiddenKeyError', + ErrorClass: ForbiddenKeyError, + entries: [['__proto__.x', '1']], + depth: Infinity, + }, + { + name: 'DepthLimitError', + ErrorClass: DepthLimitError, + entries: [['a.b', '1']], + depth: 1, + }, + ] satisfies Array<{ + name: string + ErrorClass: new (key: string) => Error + entries: Array<[string, string]> + depth: number + }>)('sets the name of $name', ({name, ErrorClass, entries, depth}) => { + const error = errorThrownBy(() => parseFormData(entries, {depth})) + expect(error).toBeInstanceOf(ErrorClass) + expect(error).toBeInstanceOf(Error) + expect(error.name).toBe(name) + expect(String(error)).toBe(`${name}: ${error.message}`) + expect(error.stack).toMatch(new RegExp(`^${name}: `)) + expect(new ErrorClass('k').name).toBe(name) + }) }) +// The error `fn` throws. Anything else comes back as a plain `Error`, which +// fails the `instanceof` check of the error class. +function errorThrownBy(fn: () => unknown): Error { + try { + fn() + } catch (error) { + return error instanceof Error ? error : new Error('threw a non-Error') + } + return new Error('did not throw') +} + describe('complex examples', () => { it('can mix array types `foo[$n].foo, -foo[$n].bar, &foo[$n+1] -> {foo: [{foo: value1, bar: null}, Boolean(Number($value3))]}`', () => { const formData = new FormData() diff --git a/src/errors.ts b/src/errors.ts index dd1550f..03afc96 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,6 @@ // The errors `parseFormData` throws. All of them are exported from the package. +// Each class sets `name` as a string literal: minifiers rename the classes, so +// `this.constructor.name` would be `e` or `t` in the minified build. /** * Thrown when a path is used multiple times or has mismatching path parts. @@ -35,6 +37,7 @@ export class DuplicateKeyError extends Error { key: string constructor(key: string) { super(`Duplicate key at path part ${key}`) + this.name = 'DuplicateKeyError' this.key = key } } @@ -64,6 +67,7 @@ export class MixedArrayError extends Error { key: string constructor(key: string) { super(`Mixed array at path part ${key}`) + this.name = 'MixedArrayError' this.key = key } } @@ -96,6 +100,7 @@ export class ForbiddenKeyError extends Error { constructor(key: string) { key = shortKey(key) super(`Forbidden key at path part ${key}`) + this.name = 'ForbiddenKeyError' this.key = key } } @@ -120,6 +125,7 @@ export class DepthLimitError extends Error { constructor(key: string) { key = shortKey(key) super(`Depth limit exceeded at path part ${key}`) + this.name = 'DepthLimitError' this.key = key } }