Specimen catalog, v1.0.0

edgegoat: edge case test inputs for JavaScript

The inputs that break code, ready to drop into your tests. Curated specimens for time zones, daylight saving time, dates before 1970, unicode test strings, -0 and NaN, URLs, paths and JSON, each with the reason it breaks code. For Vitest, Jest, node:test and Mocha.

Terminal
npm install --save-dev edgegoat
Open the lab Read the guides
A lab shelf on a mountain ridge with specimen jars holding a clock, NaN, a face split in two and a file path, next to a flask of negative zero TZ+0545 NaN -0 U+D83D ..\..\

284

specimens

11

generators

137

tags

0

dependencies

Specimen lab: every edge case in the catalog

284 specimens

All 284 edge cases. Select a row to inspect it.
No.SpecimenValueTags
001 dates() 1970-01-01T00:00:00.000Z epoch+1
002 dates() 1969-12-31T23:59:59.999Z epoch+2
003 dates() 1969-12-31T23:59:58.500Z pre-1970+2
004 dates() 1969-07-20T20:17:40.123Z pre-1970+2
005 dates() 1850-01-01T00:00:00.000Z pre-1970+2
006 dates() 0000-01-01T00:00:00.000Z far-past+1
007 dates() -000001-01-01T00:00:00.000Z far-past+1
008 dates() 2038-01-19T03:14:07.000Z y2038
009 dates() 2038-01-19T03:14:08.000Z y2038+1
010 dates() 9999-12-31T23:59:59.999Z far-future+1
011 dates() +010000-01-01T00:00:00.000Z far-future+1
012 dates() +275760-09-13T00:00:00.000Z far-future+1
013 dates() -271821-04-20T00:00:00.000Z far-past+1
014 dates() Invalid Date invalid
015 dates() 2026-03-01T00:00:00.000Z parsing+1
016 dates() 2024-02-29T12:00:00.000Z leap+1
017 dates() 2000-02-29T12:00:00.000Z leap
018 dates() 2100-02-28T12:00:00.000Z leap
019 dates() 2026-01-31T12:00:00.000Z month-end+1
020 dates() 2026-03-31T12:00:00.000Z month-end+1
021 dates() 2026-12-31T23:59:59.999Z month-end+1
022 dates() 2027-01-01T12:00:00.000Z week
023 dates() 2024-12-30T12:00:00.000Z week
024 dates() 2023-01-01T12:00:00.000Z week
025 dates() 2026-03-08T06:59:59.999Z dst+2
026 dates() 2026-03-08T07:00:00.000Z dst+2
027 dates() 2026-11-01T05:30:00.000Z dst+3
028 dates() 2026-11-01T06:30:00.000Z dst+3
029 dates() 2026-01-01T04:59:59.999Z year-boundary+1
030 dates() 2018-02-18T01:30:00.000Z dst+3
031 dates() 2018-02-18T02:30:00.000Z dst+3
032 dates() 2018-11-04T02:59:59.999Z dst+2
033 dates() 2018-11-04T03:00:00.000Z dst+2
034 dates() 2018-01-01T01:59:59.999Z year-boundary+1
035 dates() 2026-03-29T00:59:59.999Z dst+2
036 dates() 2026-03-29T01:00:00.000Z dst+2
037 dates() 2026-10-25T00:30:00.000Z dst+3
038 dates() 2026-10-25T01:30:00.000Z dst+3
039 dates() 2026-04-04T14:45:00.000Z dst+4
040 dates() 2026-04-04T15:15:00.000Z dst+4
041 dates() 2026-10-03T15:29:59.999Z dst+3
042 dates() 2026-10-03T15:30:00.000Z dst+3
043 dates() 2025-12-31T13:00:00.000Z year-boundary+1
044 dates() 2025-12-31T18:15:00.000Z year-boundary+2
045 dates() 2025-12-31T10:00:00.000Z year-boundary+1
046 dates() 2026-01-01T10:59:59.999Z year-boundary+1
047 timeZones() "UTC" utc+1
048 timeZones() "America/New_York" negative+1
049 timeZones() "America/Los_Angeles" negative+1
050 timeZones() "America/Sao_Paulo" negative+2
051 timeZones() "America/St_Johns" negative+2
052 timeZones() "Europe/London" dst+1
053 timeZones() "Europe/Dublin" dst+1
054 timeZones() "Africa/Casablanca" dst+1
055 timeZones() "Asia/Kolkata" positive+1
056 timeZones() "Asia/Kathmandu" positive+1
057 timeZones() "Asia/Tehran" positive+2
058 timeZones() "Australia/Adelaide" positive+3
059 timeZones() "Australia/Lord_Howe" positive+4
060 timeZones() "Pacific/Chatham" positive+3
061 timeZones() "Pacific/Kiritimati" positive+1
062 timeZones() "Pacific/Apia" positive+2
063 timeZones() "Pacific/Pago_Pago" negative+1
064 timeZones() "Antarctica/Troll" dst+1
065 strings() "" empty+1
066 strings() " " whitespace+1
067 strings() " \t\n\r\u000B\u000C " whitespace+1
068 strings() "a\u00A0b" whitespace+1
069 strings() "\u3000\u65E5\u672C\u3000" whitespace+1
070 strings() "foo\u200Bbar" invisible+1
071 strings() "\uFEFFid" invisible+1
072 strings() "co\u00ADoperate" invisible
073 strings() "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." (100000 chars) long
074 strings() "\u{1F600}" emoji+2
075 strings() "\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}" emoji+3
076 strings() "\u{1F1E7}\u{1F1F7}" emoji+2
077 strings() "\u{1F44D}\u{1F3FD}" emoji+2
078 strings() "\u2764\uFE0F" emoji+2
079 strings() "1\uFE0F\u20E3" emoji+2
080 strings() "abcdefghi\u{1F600}" surrogate+2
081 strings() "Done \u{1F389}" surrogate+2
082 strings() "a\uD83Db" surrogate+1
083 strings() "\uDE00" surrogate+1
084 strings() "caf\u00E9" normalization+1
085 strings() "cafe\u0301" normalization+1
086 strings() "Z\u0351\u036B\u0343\u036A\u0302\u036B\u033D\u034F\u0334\u0319" combining+1
087 strings() "\u0301abc" combining
088 strings() "\uD55C\uAD6D\uC5B4" script+2
089 strings() "\u1112\u1161\u11AB" script+2
090 strings() "abc \u05E9\u05DC\u05D5\u05DD 123" rtl+2
091 strings() "\u0645\u0631\u062D\u0628\u0627" rtl+1
092 strings() "invoice\u202Etxt.exe" bidi+2
093 strings() "user\u2067admin\u2069" bidi+2
094 strings() "I\u0130\u0131i" casing+1
095 strings() "stra\u00DFe" casing+1
096 strings() "\u039F\u0394\u039F\u03A3" casing+1
097 strings() "a\u0000b" control+1
098 strings() "line1\r\nline2" newline
099 strings() "line1\rline2" newline
100 strings() "a\u2028b" newline+1
101 strings() "a\u2029b" newline+1
102 strings() "a\u000Bb\u000Cc" control+1
103 strings() "<script>alert(\"x\")</script>" injection+1
104 strings() "&lt;b&gt; &amp;amp;" injection+1
105 strings() "Robert'); DROP TABLE Students;--" injection+1
106 strings() ".*+?^${}()|[]\\" injection+1
107 strings() "${7*7} {{7*7}} %s %d" injection+1
108 strings() "__proto__" injection+1
109 strings() "null" falsy-text
110 strings() "\u0663\u0664" digits+1
111 strings() "\uFF11\uFF12\uFF13" digits+1
112 strings() "\u0E20\u0E32\u0E29\u0E32\u0E44\u0E17\u0E22" script+1
113 strings() "\u4F60\u597D\uFF01" cjk+1
114 numbers() 0 zero+1
115 numbers() -0 zero+2
116 numbers() NaN nan+1
117 numbers() Infinity infinity
118 numbers() -Infinity infinity+1
119 numbers() 9007199254740991 integer+1
120 numbers() 9007199254740992 integer+2
121 numbers() 9007199254740992 integer+2
122 numbers() -9007199254740991 integer+2
123 numbers() -9007199254740992 integer+3
124 numbers() 1.7976931348623157e+308 limit+1
125 numbers() 5e-324 limit+1
126 numbers() 2.220446049250313e-16 limit+1
127 numbers() 4294967296 integer+1
128 numbers() 2147483648 integer+1
129 numbers() -0.5 negative+2
130 numbers() -0.0001 negative+2
131 numbers() -1e-7 negative+2
132 numbers() 1e-7 exponent+1
133 numbers() 0.30000000000000004 float+1
134 numbers() 1.005 float+1
135 numbers() 0.5 float+1
136 numbers() 1e+21 exponent+1
137 numbers() 999999999999999900000 exponent+1
138 numbers() -42 negative+1
139 numbers() 1.5 fraction
140 numericStrings() "" empty+1
141 numericStrings() " \n\t " whitespace+1
142 numericStrings() " 42 " whitespace
143 numericStrings() "-0" sign+1
144 numericStrings() "007" leading-zero
145 numericStrings() "0x1A" radix+1
146 numericStrings() "0b101" radix+1
147 numericStrings() "1e3" exponent+1
148 numericStrings() "12px" coercion
149 numericStrings() "1,5" locale
150 numericStrings() "1,234,567.89" locale
151 numericStrings() "1_000" coercion
152 numericStrings() "Infinity" infinity+1
153 numericStrings() "NaN" nan+1
154 numericStrings() "9007199254740993" precision+1
155 numericStrings() "+5" sign
156 numericStrings() "\u0664\u0662" unicode+1
157 bigints() 0n zero+1
158 bigints() -1n negative
159 bigints() 9007199254740993n precision+1
160 bigints() 9223372036854775807n int64+1
161 bigints() -9223372036854775808n int64+2
162 bigints() 18446744073709551615n uint64+1
163 bigints() 18446744073709551616n uint64+1
164 bigints() 1000000000000000000000000000000n large+1
165 urls() "http://localhost:3000/" valid+1
166 urls() "https://user:p%40ss@example.com/" valid+2
167 urls() "http://[::1]:8080/" valid+2
168 urls() "http://127.1/" valid+2
169 urls() "http://0x7f.0.0.1/" valid+2
170 urls() "https://\u4F8B\u3048.jp/" valid+3
171 urls() "https://example.com./" valid+1
172 urls() "HTTPS://EXAMPLE.COM/Path" valid+1
173 urls() "https://example.com:443/" valid+1
174 urls() "http://example.com:65536/" invalid+1
175 urls() "https://example.com/a%2Fb" valid+2
176 urls() "https://example.com/?q=a+b&q=%20c" valid+2
177 urls() "https://example.com/file%2520name" valid+1
178 urls() "https://example.com/#" valid+1
179 urls() "http://example.com/public/../secret" valid+2
180 urls() "https://example.com/\u{1F600}?q=caf\u00E9" valid+2
181 urls() "https://example.com/aaaaaaaaaaaaaaaaaaaa..." (8020 chars) valid+1
182 urls() "javascript:alert(1)" valid+2
183 urls() "java\nscript:alert(1)" valid+2
184 urls() "data:text/html,<script>alert(1)</script>" valid+2
185 urls() "file:///C:/Windows/win.ini" valid+2
186 urls() "mailto:user@example.com" valid+1
187 urls() " https://example.com/ " valid+1
188 urls() "//example.com/path" invalid+2
189 urls() "/\\example.com" invalid+2
190 urls() "example.com" invalid+1
191 urls() "http://exa mple.com/" invalid+1
192 urls() "" invalid+1
193 emails() "user+tag@example.com" valid+1
194 emails() "first.last@example.com" valid+1
195 emails() "o'brien@example.com" valid+1
196 emails() "\"john doe\"@example.com" valid+1
197 emails() "\"a@b\"@example.com" valid+1
198 emails() "user@[192.168.0.1]" valid+1
199 emails() "user@localhost" valid+1
200 emails() "USER@EXAMPLE.COM" valid+1
201 emails() "user@mail.sub.example.co.uk" valid+1
202 emails() "user@example.photography" valid+1
203 emails() "a@b.co" valid+1
204 emails() "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com" valid+2
205 emails() "jos\u00E9@example.com" valid+2
206 emails() "user@\u4F8B\u3048.jp" valid+3
207 emails() "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com" invalid+2
208 emails() "plainaddress" invalid
209 emails() "@example.com" invalid+1
210 emails() "user@" invalid+1
211 emails() "user@@example.com" invalid
212 emails() ".user@example.com" invalid+1
213 emails() "user.@example.com" invalid+1
214 emails() "us..er@example.com" invalid+1
215 emails() "user@example..com" invalid+1
216 emails() "user@-example.com" invalid+1
217 emails() "john doe@example.com" invalid+1
218 emails() "John Doe <john@example.com>" invalid+1
219 paths() "C:\\Users\\Ana\\file.txt" windows+1
220 paths() "C:/Users/Ana/file.txt" windows+2
221 paths() "C:file.txt" windows+1
222 paths() "\\temp\\file.txt" windows+1
223 paths() "\\\\server\\share\\folder\\file.txt" windows+2
224 paths() "\\\\?\\C:\\very\\long\\path" windows+2
225 paths() "C:\\segment\\segment\\segment\\segment\\segme..." (291 chars) windows+2
226 paths() "CON" windows+1
227 paths() "aux.txt" windows+1
228 paths() "file." windows+1
229 paths() "file " windows+2
230 paths() "README.md" windows+2
231 paths() "/usr/local/bin" posix+1
232 paths() "dir/sub/" posix+1
233 paths() "/a//b///c" posix+1
234 paths() "/" posix+2
235 paths() "" posix+2
236 paths() "." posix+2
237 paths() "../../etc/passwd" posix+2
238 paths() "uploads/./a/../../../secret" posix+2
239 paths() "..\\..\\Windows\\win.ini" windows+2
240 paths() ".env" posix+2
241 paths() "archive.tar.gz" posix+2
242 paths() "My Documents/file name.txt" posix+2
243 paths() "a\\b.txt" posix+1
244 paths() "caf\u00E9.txt" posix+3
245 paths() "cafe\u0301.txt" posix+2
246 paths() "notes \u{1F4DD}.md" posix+3
247 paths() "-rf" posix+1
248 paths() "a\nb.txt" posix+1
249 json() "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[..." (20000 chars) valid+1
250 json() "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":..." (60001 chars) valid+1
251 json() "{\"a\":1,\"a\":2}" valid+2
252 json() "{\"id\":9007199254740993}" valid+2
253 json() "1e400" valid+2
254 json() "-0" valid+1
255 json() "1.0" valid+1
256 json() "{\"__proto__\":{\"isAdmin\":true}}" valid+2
257 json() "{\"\":1}" valid+1
258 json() "\"\\ud800\"" valid+2
259 json() "\"a\u2028b\u2029c\"" valid+1
260 json() "\"just a string\"" valid+1
261 json() "null" valid+1
262 json() " \n\t {\"a\":1} \r\n" valid+1
263 json() "\uFEFF{\"a\":1}" invalid+1
264 json() "[1,2,]" invalid+1
265 json() "{\"a\":1 /* note */}" invalid+1
266 json() "{'a':1}" invalid+1
267 json() "{\"a\":NaN}" invalid+1
268 json() "{a:1}" invalid+1
269 json() "" invalid+1
270 json() "\"a\tb\"" invalid+1
271 arrays() [] empty
272 arrays() [1, <hole>, 3] sparse+1
273 arrays() [1, undefined, 3] undefined
274 arrays() [<hole>, <hole>, <hole>, <hole>, <hole>] sparse+1
275 arrays() ["first", <hole>, <hole>, <hole>, <hole>, <hole>, ...] (length 4294967295) sparse+2
276 arrays() [NaN] nan
277 arrays() [-0, 0] zero+1
278 arrays() [10, 9, 1, 100] sort
279 arrays() [3, undefined, 1] sort+1
280 arrays() [1, "1"] mixed+1
281 arrays() [[[]]] nested
282 arrays() [1, 2] properties
283 arrays() [1, 2, 3] frozen
284 arrays() [0, 1, 2, 3, 4, 5, ...] (length 500000) large+1

No specimen on this shelf

Nothing matches the current search and filters.

Procedure

Edge cases in a real test suite

Every generator returns an array of { label, value, why, tags } objects, so it plugs straight into test.each. Filter with { tags: [...] } or { exclude: [...] }, pick zones with dates({ zone: 'Europe/Berlin' }).

  1. Install

    Add edgegoat as a dev dependency. No configuration, no runtime dependencies.

  2. Pick the specimens

    Call a generator and filter it with { tags } or { exclude }: DST gaps, surrogate pairs, -0, path traversal.

  3. Run them with test.each

    Each case has a label for the test title and a why for the failure, so a red test tells you what broke.

Terminal
npm install --save-dev edgegoat
Terminal
pnpm add --save-dev edgegoat
Terminal
yarn add --dev edgegoat
Terminal
bun add --dev edgegoat
dates.test.ts
import { describe, expect, test } from 'vitest';
import { dates, numbers, strings, withTimeZone } from 'edgegoat';
import { addDays, formatPrice, truncate } from './my-code';

describe('addDays keeps the wall clock across DST', () => {
  test.each(dates({ tags: ['dst'] }))('$label', ({ value, zone }) => {
    withTimeZone(zone, () => {
      expect(addDays(value, 1).getHours()).toBe(value.getHours());
    });
  });
});

test.each(strings({ tags: ['surrogate'] }))('truncate keeps $label whole', ({ value }) => {
  expect(truncate(value, 5).isWellFormed()).toBe(true);
});

test.each(numbers({ tags: ['sign'] }))('formatPrice($label)', ({ value }) => {
  expect(formatPrice(value)).not.toMatch(/^-0/);
});

Field samples

Bugs these inputs found

Real bugs of this kind, fixed upstream by the author of edgegoat in widely used projects. The library packages the inputs that exposed them.

Lab notes

Guides

One bug class per guide: the bug explained, a naive implementation that fails, and the edgegoat test that catches it.

FAQ

Frequently asked questions

What is edgegoat?

A small, curated library of 284 inputs that are known to break JavaScript code: DST transitions, New Year in far away zones, dates before 1970, surrogate pairs, invisible characters, -0, NaN, Windows paths, deep JSON and sparse arrays. Every case has a label, a value, a one line reason and tags.

Which test runners does it work with?

Any of them. Generators return plain arrays of { label, value, why, tags }, so they plug into test.each in Vitest and Jest, a loop in node:test or Mocha, or a property based testing library such as fast-check.

Does edgegoat generate random data?

No. Every call returns the same cases, so failures are reproducible. DST instants are computed from the runtime tz database, so they are deterministic for a given Node.js version.

How do I run a test in another time zone?

Wrap it in withTimeZone(zone, fn). In Node.js it sets process.env.TZ, which applies to Date and Intl immediately, and restores the previous zone afterwards, even when the function throws.

Why are emoji shown as escapes in the catalog?

So you can see exactly what is in each string. Invisible characters, combining marks and surrogate pairs look identical or empty when rendered, so the catalog prints code points such as \u{1F600} instead.

Does it add weight to my bundle?

It is a dev dependency with zero runtime dependencies, shipped as ESM and CommonJS with TypeScript types. It only runs in your tests.