import {describe, it} from 'node:test'; import assert from 'node:assert/strict'; import {snakeCase} from './utils'; describe('snakeCase', () => { describe('id snake_case contract', () => { const cases: ReadonlyArray<[string, string]> = [ ['accounting', 'accounting'], ['single', 'single'], ['banking-finance', 'banking_finance'], ['customer-service', 'customer_service'], ['foo_bar', 'foo_bar'], ['foo.bar', 'foo_bar'], ['foo bar', 'foo_bar'], ['fooBar', 'foo_bar'], ['fooBarBaz', 'foo_bar_baz'], ['PascalCase', 'pascal_case'], ['kebab-case', 'kebab_case'], ['CONSTANT_CASE', 'constant_case'], ['Foo', 'foo'], ['FOO', 'foo'], ['MCF', 'mcf'], ['multiple spaces', 'multiple_spaces'], ['foo--bar', 'foo_bar'], ['123abc', '123abc'], ['foo123bar', 'foo123bar'], ['a', 'a'], ['', ''], ['XMLHttpRequest', 'xmlhttp_request'], ['-leading', '_leading'], ['trailing-', 'trailing'], ]; cases.forEach(([input, expected]) => { it(`maps "${input}" to "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('ASCII symbol separators become underscores', () => { const cases: ReadonlyArray<[string, string]> = [ ['foo/bar', 'foo_bar'], ['foo/bar/baz', 'foo_bar_baz'], ['foo#bar', 'foo_bar'], ['foo!bar', 'foo_bar'], ['foo@bar', 'foo_bar'], ['foo&bar', 'foo_bar'], ['foo(bar)', 'foo_bar'], ['foo[bar]', 'foo_bar'], ['foo,bar', 'foo_bar'], ['foo;bar', 'foo_bar'], ['foo:bar', 'foo_bar'], ['foo+bar', 'foo_bar'], ['foo=bar', 'foo_bar'], ['foo|bar', 'foo_bar'], ['foo\\bar', 'foo_bar'], ['foo^bar', 'foo_bar'], ['foo~bar', 'foo_bar'], ['foo%bar', 'foo_bar'], ['foo*bar', 'foo_bar'], ['foo?bar', 'foo_bar'], ['foo', 'foo_bar'], ['a/b_c-d.e f', 'a_b_c_d_e_f'], ]; cases.forEach(([input, expected]) => { it(`turns "${input}" into "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('apostrophe is removed without a separator (quirk)', () => { it("does not insert an underscore for apostrophes (foo'bar -> foobar)", () => { assert.strictEqual(snakeCase("foo'bar"), 'foobar'); }); }); describe('whitespace handling', () => { const cases: ReadonlyArray<[string, string]> = [ ['foo bar', 'foo_bar'], ['multiple spaces', 'multiple_spaces'], ['foo\tbar', 'foo\tbar'], ['foo\nbar', 'foo\nbar'], ]; cases.forEach(([input, expected]) => { it(`maps whitespace ${JSON.stringify(input)} to ${JSON.stringify(expected)}`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('leading, trailing and consecutive separators', () => { const cases: ReadonlyArray<[string, string]> = [ ['#foo', '_foo'], ['/leading-slash', '_leading_slash'], ['trailing-slash/', 'trailing_slash'], ['foo...bar', 'foo_bar'], ['---', ''], ['___', ''], ['...', ''], ['100%', '100'], ['C++', 'c'], ]; cases.forEach(([input, expected]) => { it(`maps "${input}" to "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('accented and extended Latin (French, German, Spanish, Nordic)', () => { const cases: ReadonlyArray<[string, string]> = [ ['café', 'café'], ['CAFÉ', 'café'], ['École', 'école'], ['crème brûlée', 'crème_brûlée'], ['déjà-vu', 'déjà_vu'], ['naïve', 'naïve'], ['hôtel', 'hôtel'], ['Müller', 'müller'], ['größe', 'größe'], ['niño', 'niño'], ['Ångström', 'ångström'], ['ÆON', 'æon'], ['Œuvre', 'œuvre'], ['à la carte', 'à_la_carte'], ]; cases.forEach(([input, expected]) => { it(`preserves and case-folds "${input}" to "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('CJK characters (Chinese, Japanese, Korean)', () => { const cases: ReadonlyArray<[string, string]> = [ ['软件开发', '软件开发'], ['软件开发人员', '软件开发人员'], ['软件-开发', '软件_开发'], ['软件 开发', '软件_开发'], ['ソフトウェア', 'ソフトウェア'], ['ソフトウェア-開発', 'ソフトウェア_開発'], ['소프트웨어', '소프트웨어'], ['software开发', 'software开发'], ['JOB开发', 'job开发'], ]; cases.forEach(([input, expected]) => { it(`preserves CJK "${input}" as "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('emoji', () => { const cases: ReadonlyArray<[string, string]> = [ ['foo🤡bar', 'foo🤡bar'], ['a🤡b🎉c', 'a🤡b🎉c'], ['🤡', '🤡'], ]; cases.forEach(([input, expected]) => { it(`preserves emoji inline for "${input}" as "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('unicode dashes and quotes (quirks)', () => { it('does not treat en dash (–) as a separator', () => { assert.strictEqual(snakeCase('foo–bar'), 'foo–bar'); }); it('does not treat em dash (—) as a separator', () => { assert.strictEqual(snakeCase('foo—bar'), 'foo—bar'); }); it('does not treat curly quotes (“ ”) as separators', () => { assert.strictEqual(snakeCase('foo“bar”'), 'foo“bar”'); }); it('treats guillemets (« ») as separators', () => { assert.strictEqual(snakeCase('foo«bar»'), 'foo_bar'); }); }); describe('other scripts and full-width characters', () => { const cases: ReadonlyArray<[string, string]> = [ ['Тест', 'тест'], ['Ελληνικά', 'ελληνικά'], ['رئيس', 'رئيس'], ['ABC', 'abc'], ['foo', 'foo'], ]; cases.forEach(([input, expected]) => { it(`maps "${input}" to "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('mixed casing and digits', () => { const cases: ReadonlyArray<[string, string]> = [ ['iPhone', 'i_phone'], ['B2B', 'b2b'], ['node.js', 'node_js'], ['F#', 'f'], ]; cases.forEach(([input, expected]) => { it(`maps "${input}" to "${expected}"`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('case.snake parity (previously divergent inputs)', () => { const cases: ReadonlyArray<[string, string]> = [ ['aXX', 'a_x_x'], ['a1X', 'a1_x'], ['a-\n', 'a-\n'], ["a-'", 'a_'], ]; cases.forEach(([input, expected]) => { it(`maps ${JSON.stringify(input)} to ${JSON.stringify(expected)}`, () => { assert.strictEqual(snakeCase(input), expected); }); }); }); describe('properties', () => { it('is deterministic: identical inputs produce identical results', () => { assert.strictEqual(snakeCase('foo-bar'), snakeCase('foo-bar')); }); it('is idempotent: the transform is stable under repeated application', () => { const result = snakeCase('banking-finance'); assert.strictEqual(result, 'banking_finance'); assert.strictEqual(snakeCase(result), result); }); }); });