universal-emoji-parser
Version:
This tool allow parse unicode and emoji codes to html images using emojilib && Twemoji CDN
246 lines (173 loc) • 10.1 kB
Markdown
# Testing Guide
How to write and run tests in Universal Emoji Parser. The package uses **Mocha + Chai 6** with **tsx** so specs run as `.ts` directly — no separate compile step (Chai 6 is ESM-first; the test script uses `tsx` + Mocha).
## Where tests live
All specs live in `test/*.test.ts`:
| File | Coverage |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `test/main.test.ts` | The public API: `parse`, `parseToHtml`, `parseToUnicode`, `parseToShortcode`, plus error cases and option permutations |
| `test/emojiLibJson.test.ts` | Catalog metadata: total count (`TOTAL_EMOJIS = 1906`), shape of `EmojiType` entries, presence of canonical emojis |
| `test/prepareEmojiLibJson.test.ts` | The regenerator — `it.skip`-guarded; runs only when explicitly enabled to rebuild `src/lib/emoji-lib.json` |
There is no separation by source-file ↔ test-file pairing — `main.test.ts` covers everything in `src/index.ts` because the API is small.
## Running tests
```bash
npm test # Everything
npm run test:watch # Re-runs on file change (TDD inner loop)
# Single file
npx tsx ./node_modules/mocha/bin/mocha.js test/main.test.ts --colors
# Filter by name
npx tsx ./node_modules/mocha/bin/mocha.js test/main.test.ts --grep "should parse" --colors
# Single it()
npx tsx ./node_modules/mocha/bin/mocha.js test/main.test.ts --grep "should throw error with not string parameter" --colors
```
Mocha config is inline in the `test` script: `--timeout 25000 --colors`. The 25-second timeout exists for one slow path (the regenerator's O(n²) dedup loop); regular specs finish in milliseconds.
## Conventions
### File and test naming
```ts
describe('Test emoji parser', () => { // Top-level: subject under test
describe('Using default options', () => { // Nested: scenario / configuration
it('should parse emojis from unicode', () => { ... }) // Single behavior
it('should parse emojis from shortcode', () => { ... })
})
describe('Using custom options', () => {
it('should parse emojis to shortcode only', () => { ... })
})
})
```
- **Top-level `describe`** matches the module under test (`Test emoji parser`, `Test emoji lib json data`)
- **Nested `describe`** groups by scenario (`Using default options`, `Validate json data`)
- **`it` names start with "should"** describing observable behavior — never internal mechanics
### One behavior per `it`
If you need "and" in an `it` name, split the test:
```ts
// ❌ tests two behaviors
it('should parse emojis and handle the empty string', ...)
// ✅
it('should parse emojis from unicode', ...)
it('should return empty string when input is empty', ...)
```
### Arrange / Act / Assert structure
```ts
it('should parse emojis from shortcode', () => {
// Arrange — declare inputs + expectations
const text: string = ':smile:'
// Act — call the code under test
const result: string = uEmojiParser.parse(text)
// Assert — verify the output
expect(result).to.be.equal(
'<img class="emoji" alt="🙂" src="https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/1f642.svg"/>'
)
})
```
Most existing tests use a single statement style (no comment markers); the explicit AAA blocks above are recommended for new tests where the sections aren't obvious. The existing tests **don't** use blank-line section separators — keep that style for consistency.
## Chai assertion idioms used in this repo
```ts
import { expect } from 'chai'
// Equality (primitives)
expect(result).to.be.equal('expected')
expect(count).to.be.equal(1906)
// Deep equality (objects, arrays)
expect(emojiObject).to.be.deep.equal({ name: '...', slug: '...', ... })
// Type checks
expect(arr).to.be.an('array')
expect(obj).to.be.an('object')
expect(str).to.be.a('string')
// Length
expect(emojiLibJsonDataKeys.length).to.be.equal(TOTAL_EMOJIS)
// Throws
expect(() => { uEmojiParser.parse(text) }).to.throw(Error)
// File system existence
expect(fs.existsSync(filePath)).to.be.true // requires // eslint-disable-next-line @typescript-eslint/no-unused-expressions
```
> **Note**: Chai's `expect(x).to.be.true` style is a "no-op expression" by ESLint rules. Suppress with `// eslint-disable-next-line @typescript-eslint/no-unused-expressions` immediately above the line.
## Catalog tests (`emojiLibJson.test.ts`)
This file validates the curated catalog. Every assertion is **deterministic** — the catalog is committed JSON, so the count and entries are exact:
```ts
const TOTAL_EMOJIS: number = 1906
expect(emojiLibJsonDataKeys.length).to.be.equal(TOTAL_EMOJIS)
```
If you regenerate the catalog and the count changes, **update `TOTAL_EMOJIS`** in this file as part of the same commit. The PR diff makes the count change reviewable.
The deep-equal checks pin the shape of specific emojis (🤣, 😎). Update these only if the upstream `unicode-emoji-json` changes the metadata — typically you don't.
## The regenerator (`prepareEmojiLibJson.test.ts`)
This is the **only** way to rebuild `src/lib/emoji-lib.json`. Key facts:
- **`it.skip(...)`** — disabled by default; running `npm test` does not regenerate
- Imports `emojilib` and `unicode-emoji-json` (devDependencies) and merges them
- Applies `EMOJIS_SPECIAL_CASES` overrides (include/exclude per emoji)
- Runs an O(n²) dedup loop to ensure each keyword appears on at most one emoji (the most-canonical one)
- Writes `src/lib/emoji-lib-output.json` (gitignored)
Procedure to regenerate:
1. Edit `it.skip(...)` → `it(...)` in `prepareEmojiLibJson.test.ts`
2. `npm test` — the test runs and writes the output file
3. `diff src/lib/emoji-lib.json src/lib/emoji-lib-output.json` — review changes
4. `cp src/lib/emoji-lib-output.json src/lib/emoji-lib.json` if happy
5. Update `TOTAL_EMOJIS` in `emojiLibJson.test.ts` if the count changed
6. **Restore `it.skip`** in `prepareEmojiLibJson.test.ts`
7. `npm test` (without `.skip` reverted, this would re-run; with skip it's safe)
8. Commit all four files together: `prepareEmojiLibJson.test.ts`, `emoji-lib.json`, `emojiLibJson.test.ts`, and any test changes that motivated the regen
Full walkthrough: [`/regenerate-emoji-lib`](../.agents/commands/regenerate-emoji-lib.md).
## What to test
**Always:**
- A new public method or option flag — exhaustive permutations of the new dimension
- A regression — paste the failing input verbatim, assert the corrected output
- A new keyword/alias added via `EMOJIS_SPECIAL_CASES` — at least one test that resolves the new shortcode
**Sometimes:**
- HTML output snapshots — when adding a new emoji-rendering path. Be aware these are large and noisy in diffs
- Internal helpers (`getEmojiObjectByShortcode`, `getDefaultOptions`) — when changing their behavior; they're typed as part of `UEmojiParserType`, so consumers can technically rely on them
**Rarely:**
- Twemoji's own behavior — that's covered by the upstream package. Don't test that "🚀" → some-Twemoji-URL; just test that _our_ code correctly hands off to `@twemoji/parser`
- Performance regressions — we don't have benchmark infrastructure. If you add one, also add a doc page
## Adding a regression test
When fixing a parsing bug:
1. **Get the exact input.** Copy-paste from the bug report; preserve every byte (variation selectors like `️`, ZWJ sequences, etc. matter)
2. **Stand it up as a failing test:**
```ts
it('should parse :star: even with VS-16 (regression #123)', () => {
const text: string = ':star:️' // note the trailing variation selector
const result: string = uEmojiParser.parse(text)
expect(result).to.contain('alt="⭐️"')
})
```
3. Run `npm run test:watch`; watch the new test fail
4. Fix `src/index.ts` until it passes
5. Commit fix + test together with a `fix:` conventional message
## Snapshot-style assertions
The integration tests in `main.test.ts` use **literal expected output strings** for full HTML, not a snapshot library. This is deliberate:
- The diff shows up clearly in PR review (no opaque `__snapshots__` folder)
- Updating an expectation requires intent — it's a real code change
- No additional dev dependency
When adding a new snapshot-style assertion, manually run the input through the current code, paste the output literally, then verify it visually before committing.
## Coverage
There is **no coverage tool wired**. To add one (Istanbul / nyc):
```bash
npm install --save-dev nyc
```
Update `package.json`:
```json
{
"scripts": {
"test": "nyc tsx ./node_modules/mocha/bin/mocha.js 'test/**/*.ts' --timeout 25000 --colors",
"coverage": "nyc report --reporter=text-summary"
},
"nyc": {
"extension": [".ts"],
"include": ["src/**/*.ts"],
"exclude": ["**/*.test.ts"],
"reporter": ["text", "html"],
"all": true
}
}
```
Then `npm test` produces a coverage report and `coverage/` (add to `.gitignore`). Document the addition in [Technologies](TECHNOLOGIES.md) and update this file.
## Speed tips
- `npm run test:watch` is the fastest loop — sub-second after the initial Mocha startup
- Filter aggressively with `--grep` when iterating on a single behavior
- The regenerator test (`prepareEmojiLibJson.test.ts`) is **slow** when un-skipped — O(n²) over 1906 emojis takes ~10 seconds. Don't enable it just to "see what happens"
## Mocking
The package has no external services — everything is in-memory data + a synchronous Twemoji call. There is **no mocking** in the test suite. Adding a mock is a smell; if you find yourself wanting one, the production code probably has IO it shouldn't have.
## Pre-push checklist
```bash
npm run eslint:check
npm run prettier:check
npm test
npm run build
```
All four must succeed. CI runs the same set on every PR.