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.
284
specimens
11
generators
137
tags
0
dependencies
Specimen lab: every edge case in the catalog
| No. | Specimen | Value | Tags |
|---|---|---|---|
| 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() | "<b> &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' }).
Install
Add edgegoat as a dev dependency. No configuration, no runtime dependencies.
Pick the specimens
Call a generator and filter it with
{ tags }or{ exclude }: DST gaps, surrogate pairs,-0, path traversal.Run them with test.each
Each case has a
labelfor the test title and awhyfor the failure, so a red test tells you what broke.
npm install --save-dev edgegoatpnpm add --save-dev edgegoatyarn add --dev edgegoatbun add --dev edgegoatimport { 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.
- Sample Agithub/relative-time-element#384
The "is this the current year?" check used UTC instead of the display time zone, so dates near New Year showed or hid the year incorrectly.
- Sample Bmozilla/pdf.js#22017
util.printf dropped the sign of negative numbers greater than -1, such as -0.5.
- Sample Cgoogle/budoux#1411
Text segmentation could split a surrogate pair, producing half an emoji.
- Sample Dmicrosoft/vscode-js-debug#2423
trimEnd style truncation could cut a surrogate pair in half.
- Sample Esalesforce/tough-cookie#644
A cookie with an infinite max-age has to be treated as never expiring.
- Sample Fvuejs/core#15618
A fix in the Vue 3 compat build.
Lab notes
Guides
One bug class per guide: the bug explained, a naive implementation that fails, and the edgegoat test that catches it.
- TimeHow to test time zones in JavaScriptTest JavaScript date code in any time zone: run tests under TZ, switch zones per test with withTimeZone, and catch New Year and UTC offset bugs with ready made edge cases.18 specimens
- TimeHow to test daylight saving time in JestTest DST transitions in Jest: real spring forward gaps and fall back overlaps for New York, London, Sao Paulo and Lord Howe, computed from Intl, plus mocking the date without fake timers.16 specimens
- UnicodeUnicode test strings for JavaScriptReady to use unicode test strings for JavaScript: ZWJ emoji, flags, lone surrogates, NFC vs NFD, bidi controls, invisible spaces and Turkish i, each with the reason it breaks code.23 specimens
- NumbersJavaScript number edge cases to testTest JavaScript number handling with -0, NaN, Infinity, MAX_SAFE_INTEGER + 1, 0.1 + 0.2, 1e21 and values between -1 and 0, plus numeric strings and BigInt limits.26 specimens
- TimeTesting dates before 1970 in JavaScriptDates before the Unix epoch have negative timestamps, and fractional seconds round the wrong way with Math.trunc and %. Test pre-1970 dates, local mean time and year 0 with edgegoat.8 specimens
- NumbersNegative zero in JavaScript: why -0 is different from 0-0 === 0 is true, yet 1 / -0 is -Infinity and Object.is(-0, 0) is false. Learn where -0 comes from, which code it breaks, and how to test it.4 specimens
- TimeHow to mock Date in JavaScript testsFreeze new Date() and Date.now() in Vitest, Jest, node:test or Mocha with a tiny helper, combine it with a time zone, and test New Year and DST edge instants.12 specimens
- MethodProperty based testing and curated edge cases in JavaScriptProperty based testing finds bugs with random inputs; curated edge cases find the known ones every time. How to combine fast-check with edgegoat's labeled inputs in Vitest or Jest.8 specimens
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.