Added scanning barcodes with a camera

This commit is contained in:
2026-03-08 16:59:33 +00:00
parent b4f8489834
commit 5a37e5dd5f
404 changed files with 224181 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
# Integration Tests
## Test Infrastructure
This directory contains integration tests for Quagga2's barcode decoder functionality. The tests have been organized into separate files per decoder type for better maintainability.
### File Structure
- `helpers.ts` - Shared test utilities and configuration functions
- `integration.spec.ts` - General integration tests (parallel decoding, edge cases)
- `decoders/` - Individual decoder test files:
- `ean.spec.ts` - EAN-13 barcode tests
- `ean_extended.spec.ts` - EAN with supplements (EAN-2, EAN-5)
- `ean_8.spec.ts` - EAN-8 barcode tests
- `upc.spec.ts` - UPC-A barcode tests
- `upc_e.spec.ts` - UPC-E barcode tests
- `code_128.spec.ts` - Code 128 barcode tests
- `code_39.spec.ts` - Code 39 barcode tests
- `code_39_vin.spec.ts` - Code 39 VIN (Vehicle Identification Number) tests
- `code_32.spec.ts` - Code 32 (Italian Pharmacode) tests
- `code_93.spec.ts` - Code 93 barcode tests
- `codabar.spec.ts` - Codabar barcode tests
- `i2of5.spec.ts` - Interleaved 2 of 5 barcode tests
- `2of5.spec.ts` - Standard 2 of 5 barcode tests
- `external-reader.spec.ts` - Tests for external reader functionality
### Test Behavior
By default, all decoder tests **must pass in both Node and browser environments**. If a test fails, the entire test run fails, alerting developers to regressions.
For tests that are known to fail in specific environments, you can use environment-specific flags to mark them explicitly. **These flags are the single authoritative source** for test failure configuration.
### Test Helper: `it.allowFail()`
The `it.allowFail()` helper is used internally when a test is marked with environment-specific failure flags. When a test fails in an allowed environment, it will be marked as "pending" instead of causing the test run to fail.
### Marking Tests with Environment-Specific Flags
In the test data structures, you can mark individual test cases with explicit environment-specific failure policies:
- **`allowFailInNode: true`**: Test can fail in Node environment without failing the build
- **`allowFailInBrowser: true`**: Test can fail in browser environment without failing the build
- **Both flags**: Test can fail in both environments - set both flags explicitly
```typescript
runDecoderTest('code_128', generateConfig(), [
// This test passes everywhere - no flags needed, will fail build if it breaks
{ 'name': 'image-001.jpg', 'result': '0001285112001000040801', format: 'code_128' },
// This test passes in Node but fails in browser - use allowFailInBrowser only
{ 'name': 'image-003.jpg', 'result': '673023', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
// This test fails in both environments - set BOTH flags explicitly
{ 'name': 'failing-test.jpg', 'result': '123456', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
]);
```
## Current Test Status
As of the latest update:
- **399 tests passing** (improved from 387, +12 tests)
- **~54 tests pending** in both Node and browser (down from 64)
- Tests are balanced between environments
### Configuration Improvements
Recent optimizations have significantly improved decoder accuracy:
**EAN-8 Decoder:**
- Changed `patchSize` from `'medium'` to `'large'` for better accuracy
- Fixed: image-004 now decodes correctly with halfSample:true
- Trade-off: image-003 now fails in browser with halfSample:false (marked with `allowFailInBrowser`)
**Code 39 VIN Decoder:**
- Increased `inputStream.size` from 1280 → 2000 → **2200** (2x the 1100px original image size)
- Fixed: 5 images now pass (001, 003, 005, 006, 011) - improved from only 1 passing
- Note: 6 images still fail (002, 004, 007, 008, 009, 010) even with optimal settings - marked with both allowFail flags
- Testing revealed performance peaks around 2x: 3x and 4x scaling both perform worse (5/11 vs 10/11 passing)
**Interleaved 2 of 5 (i2of5) Decoder:**
- Set `inputStream.size` to **1375** (1.25x the 1100px original)
- **Perfect accuracy**: All 5 test images now pass in both halfSample modes (10/10 tests)
- Testing showed 1.25x-1.5x work well for these test images
- Performance degrades at higher scaling: 2.5x causes complete failure in halfSample:false mode
**Key Insight - Upscaling Improves Detection:**
Contrary to conventional wisdom, **upscaling images can significantly improve barcode detection accuracy**. Testing showed:
- Upscaling improves detection in **both** halfSample:true and halfSample:false modes
- Integer scaling factors (2x) provide clean pixel doubling with minimal interpolation artifacts
- Optimal scaling varies by image content and quality, not necessarily by barcode type
- Performance typically peaks at moderate upscaling (1.25x-2x) and degrades beyond 2.5x
- The interpolation acts as a smoothing filter, providing more pixels per bar for the locator to analyze
### Decoders with Targeted Configurations
- **ean_8**: Uses `patchSize: 'large'` (improved accuracy)
- **code_39_vin**: Uses `inputStream.size: 2200` (2x scaling for optimal accuracy)
- **i2of5**: Uses `inputStream.size: 1375` and `patchSize: 'small'` (1.25x scaling, perfect 100% accuracy)
- **code_32**: Uses `patchSize: 'large'` and `inputStream.size: 1280`
- **code_93**: Uses `patchSize: 'large'`
## Running Tests
```bash
# Run all integration tests in Node
npx ts-mocha -p test/tsconfig.json test/integration/**/*.spec.ts
# Run all tests (including integration tests)
npm run test:node
# Run browser tests (requires Cypress)
npm run test:browser-all
```
## Adding New Tests
When adding new decoder test cases:
1. Add the test data to the appropriate `runDecoderTest()` call **without** any flags
2. Run the tests in both Node and browser environments
3. Based on the results, add explicit flags:
- **Passes everywhere**: Leave without flags
- **Fails only in Node**: Add `allowFailInNode: true`
- **Fails only in browser**: Add `allowFailInBrowser: true`
- **Fails in both**: Add **both** `allowFailInNode: true` and `allowFailInBrowser: true`
Example:
```typescript
runDecoderTest('my_decoder', generateConfig(), [
// Passes everywhere - no flags needed
{ 'name': 'working-image.jpg', 'result': '123456', format: 'my_format' },
// Passes in Node, fails in browser - set allowFailInBrowser only
{ 'name': 'browser-issue.jpg', 'result': '789012', format: 'my_format', allowFailInBrowser: true },
// Fails in both environments - set BOTH flags explicitly
{ 'name': 'problematic-image.jpg', 'result': '345678', format: 'my_format', allowFailInNode: true, allowFailInBrowser: true },
]);
```
## Fixing Failing Tests
When you fix a test that was marked with failure flags:
**For tests with both `allowFailInNode` and `allowFailInBrowser`:**
1. Remove both flags from the test
2. Verify the test passes consistently in both Node and browser environments
3. The test will now fail the build if it breaks in either environment
**For tests with `allowFailInBrowser` only:**
1. Fix the browser-specific issue
2. Remove the `allowFailInBrowser: true` flag
3. Verify the test passes in both Node and browser
4. The test will now fail the build if it breaks in either environment
**For tests with `allowFailInNode` only:**
1. Fix the Node-specific issue
2. Remove the `allowFailInNode: true` flag
3. Verify the test passes in both Node and browser
4. The test will now fail the build if it breaks in either environment
## Design Philosophy
The default behavior is "tests must pass in both environments" to catch regressions early. Environment-specific failure flags provide explicit control:
- **Single source of truth** - Test item flags in the spec files are the authoritative configuration
- **Regressions are caught immediately** - If a working test breaks, the build fails
- **Environment-specific exceptions** - Tests can be marked to allow failure in specific environments only
- **No implicit behavior** - Flags must be set explicitly for each environment
- **Clear intent** - Flags clearly indicate which environments have known issues
## Browser vs Node Differences
CI runs integration tests in **both Cypress (browser) and ts-node (Node.js)**. Some tests behave differently between these environments due to differences in image processing (Browser uses native Canvas API, Node uses the `canvas` package).
### Configuration Trade-offs
When optimizing decoder configurations for accuracy, some changes may improve one test while causing another to fail. These trade-offs are documented with comments in the decoder spec files and marked with appropriate failure flags.
Example: Changing EAN-8's `patchSize` to `'large'` fixed image-004 but caused image-003 to fail in the browser environment. The net result is still positive (more tests passing overall), and the failure is explicitly marked.

View File

@@ -0,0 +1,25 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('2 of 5 Decoder Tests', () => {
const twoOf5TestSet = [
{ 'name': 'image-001.jpg', 'result': '9577149002', format: '2of5' },
{ 'name': 'image-002.jpg', 'result': '9577149002', format: '2of5' },
{ 'name': 'image-003.jpg', 'result': '5776158811', format: '2of5' },
{ 'name': 'image-004.jpg', 'result': '0463381455', format: '2of5' },
{ 'name': 'image-005.jpg', 'result': '3261594101', format: '2of5', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-006.jpg', 'result': '3261594101', format: '2of5', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-007.jpg', 'result': '3261594101', format: '2of5' },
{ 'name': 'image-008.jpg', 'result': '6730705801', format: '2of5' },
{ 'name': 'image-009.jpg', 'result': '5776158811', format: '2of5' },
{ 'name': 'image-010.jpg', 'result': '5776158811', format: '2of5' },
];
runDecoderTestBothHalfSample('2of5', (halfSample) => generateConfig({
inputStream: { size: 800, singleChannel: false },
locator: {
halfSample,
},
decoder: {
readers: ['2of5_reader'],
},
}), twoOf5TestSet);
});

View File

@@ -0,0 +1,24 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Codabar Decoder Tests', () => {
const codabarTestSet = [
{ 'name': 'image-001.jpg', 'result': 'A10/53+17-70D', format: 'codabar' },
{ 'name': 'image-002.jpg', 'result': 'B546745735B', format: 'codabar' },
{ 'name': 'image-003.jpg', 'result': 'C$399.95A', format: 'codabar' },
{ 'name': 'image-004.jpg', 'result': 'B546745735B', format: 'codabar' },
{ 'name': 'image-005.jpg', 'result': 'C$399.95A', format: 'codabar' },
{ 'name': 'image-006.jpg', 'result': 'B546745735B', format: 'codabar' },
{ 'name': 'image-007.jpg', 'result': 'C$399.95A', format: 'codabar' },
{ 'name': 'image-008.jpg', 'result': 'A16:9/4:3/3:2D', format: 'codabar', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-009.jpg', 'result': 'C$399.95A', format: 'codabar' },
{ 'name': 'image-010.jpg', 'result': 'C$399.95A', format: 'codabar' },
];
runDecoderTestBothHalfSample('codabar', (halfSample) => generateConfig({
locator: {
halfSample,
},
decoder: {
readers: ['codabar_reader']
}
}), codabarTestSet);
});

View File

@@ -0,0 +1,44 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Code 128 Decoder Tests', () => {
// Note: FNC1 characters are represented as ASCII 29 (Group Separator, \x1D or \u001d)
// These are used in GS1-128 barcodes as field separators
const FNC1 = String.fromCharCode(29);
const code128TestSet = [
{ 'name': 'image-001.jpg', 'result': '0001285112001000040801', format: 'code_128' },
{ 'name': 'image-002.jpg', 'result': 'FANAVF14617104', format: 'code_128' },
{ 'name': 'image-003.jpg', 'result': '673023', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-004.jpg', 'result': '010210150301625334', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-005.jpg', 'result': '419055603900009001012999', format: 'code_128' },
{ 'name': 'image-006.jpg', 'result': '419055603900009001012999', format: 'code_128' },
// GS1-128 barcode with FNC1 characters as field separators
{ 'name': 'image-007.jpg', 'result': `${FNC1}42095747${FNC1}9499907123456123456781`, format: 'code_128' },
{ 'name': 'image-008.jpg', 'result': '1020185021797280784055', format: 'code_128' },
{ 'name': 'image-009.jpg', 'result': '0001285112001000040801', format: 'code_128' },
{ 'name': 'image-010.jpg', 'result': '673023', format: 'code_128' },
// TODO: need to implement having different inputStream parameters to be able to
// read this one -- it works only with inputStream size set to 1600 presently, but
// other samples break at that high a size.
// { name: 'image-011.png', result: '33c64780-a9c0-e92a-820c-fae7011c11e2' },
// GS1-128 barcodes from issue #390 - real-world food packaging barcodes
// image-012 works with halfSample: false, but not with halfSample: true
{ 'name': 'image-012.jpg', 'result': '01906641589574681121102531020003402152731515', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
// image-013 and image-014 require higher resolution settings to decode properly
// According to issue #390, image-013 needs size: 1280, patchSize: 'small'
// and image-014 needs size: 1600, patchSize: 'large'
{ 'name': 'image-013.jpg', 'result': '', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-014.jpg', 'result': '', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
];
runDecoderTestBothHalfSample('code_128', (halfSample) => generateConfig({
inputStream: {
size: 800,
singleChannel: false,
},
locator: {
halfSample,
},
decoder: {
readers: ['code_128_reader'],
},
}), code128TestSet);
});

View File

@@ -0,0 +1,29 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Code 32 Decoder Tests', () => {
const code32TestSet = [
{ name: 'image-1.jpg', result: 'A123456788', format: 'code_32_reader' },
{ name: 'image-2.jpg', result: 'A931028462', format: 'code_32_reader', allowFailInNode: true },
{ name: 'image-3.jpg', result: 'A931028462', format: 'code_32_reader', allowFailInNode: true },
{ name: 'image-4.jpg', result: 'A935776043', format: 'code_32_reader' },
{ name: 'image-5.jpg', result: 'A935776043', format: 'code_32_reader' },
{ name: 'image-6.jpg', result: 'A012745182', format: 'code_32_reader', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-7.jpg', result: 'A029651039', format: 'code_32_reader', allowFailInNode: true },
{ name: 'image-8.jpg', result: 'A029651039', format: 'code_32_reader', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-9.jpg', result: 'A015896018', format: 'code_32_reader' },
{ name: 'image-10.jpg', result: 'A015896018', format: 'code_32_reader' },
];
runDecoderTestBothHalfSample('code_32', (halfSample) => generateConfig({
inputStream: {
size: 1280,
},
locator: {
patchSize: 'large',
halfSample,
},
numOfWorkers: 4,
decoder: {
readers: ['code_32_reader']
}
}), code32TestSet);
});

View File

@@ -0,0 +1,25 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Code 39 Decoder Tests', () => {
const code39TestSet = [
{ 'name': 'image-001.jpg', 'result': 'B3% $DAD$', format: 'code_39' },
{ 'name': 'image-003.jpg', 'result': 'CODE39', format: 'code_39' },
{ 'name': 'image-004.jpg', 'result': 'QUAGGAJS', format: 'code_39' },
{ 'name': 'image-005.jpg', 'result': 'CODE39', format: 'code_39', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-006.jpg', 'result': '2/4-8/16-32', format: 'code_39' },
{ 'name': 'image-007.jpg', 'result': '2/4-8/16-32', format: 'code_39' },
{ 'name': 'image-008.jpg', 'result': 'CODE39', format: 'code_39' },
{ 'name': 'image-009.jpg', 'result': '2/4-8/16-32', format: 'code_39' },
// TODO: image 10 in this set appears to be dependent upon #191
{ 'name': 'image-010.jpg', 'result': 'CODE39', format: 'code_39' },
{ 'name': 'image-011.jpg', 'result': '4', format: 'code_39', allowFailInNode: true, allowFailInBrowser: true },
];
runDecoderTestBothHalfSample('code_39', (halfSample) => generateConfig({
locator: {
halfSample,
},
decoder: {
readers: ['code_39_reader'],
}
}), code39TestSet);
});

View File

@@ -0,0 +1,29 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Code 39 VIN Decoder Tests', () => {
const code39VinTestSet = [
{ name: 'image-001.jpg', result: '2HGFG1B86BH501831', format: 'code_39_vin' },
{ name: 'image-002.jpg', result: 'JTDKB20U887718156', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-003.jpg', result: 'JM1BK32G071773697', format: 'code_39_vin' },
{ name: 'image-004.jpg', result: 'WDBTK75G94T028954', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-005.jpg', result: '3VW2K7AJ9EM381173', format: 'code_39_vin' },
{ name: 'image-006.jpg', result: 'JM1BL1H4XA1335663', format: 'code_39_vin' },
{ name: 'image-007.jpg', result: 'JHMGE8H42AS021233', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-008.jpg', result: 'WMEEJ3BA4DK652562', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true },
{ name: 'image-009.jpg', result: 'WMEEJ3BA4DK652562', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true }, //yes, 8 and 9 are same barcodes, different images slightly
{ name: 'image-010.jpg', result: 'WMEEJ3BA4DK652562', format: 'code_39_vin', allowFailInNode: true, allowFailInBrowser: true }, // 10 also
{ name: 'image-011.jpg', result: '5FNRL38488B411196', format: 'code_39_vin' },
];
runDecoderTestBothHalfSample('code_39_vin', (halfSample) => generateConfig({
inputStream: {
size: 2200, // 2x scaling (from 1100x658) provides optimal detection accuracy
sequence: false,
},
locator: {
halfSample,
},
decoder: {
readers: ['code_39_vin_reader'],
},
}), code39VinTestSet);
});

View File

@@ -0,0 +1,27 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Code 93 Decoder Tests', () => {
const code93TestSet = [
{ 'name': 'image-001.jpg', 'result': 'WIWV8ETQZ1', format: 'code_93' },
{ 'name': 'image-002.jpg', 'result': 'EH3C-%GU23RK3', format: 'code_93' },
{ 'name': 'image-003.jpg', 'result': 'O308SIHQOXN5SA/PJ', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-004.jpg', 'result': 'DG7Q$TV8JQ/EN', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-005.jpg', 'result': 'DG7Q$TV8JQ/EN', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-006.jpg', 'result': 'O308SIHQOXN5SA/PJ', format: 'code_93' },
{ 'name': 'image-007.jpg', 'result': 'VOFD1DB5A.1F6QU', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-008.jpg', 'result': 'WIWV8ETQZ1', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-009.jpg', 'result': '4SO64P4X8 U4YUU1T-', format: 'code_93' },
{ 'name': 'image-010.jpg', 'result': '4SO64P4X8 U4YUU1T-', format: 'code_93', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-011.jpg', result: '11169', format: 'code_93' },
];
runDecoderTestBothHalfSample('code_93', (halfSample) => generateConfig({
inputStream: { size: 800, singleChannel: false },
locator: {
patchSize: 'large',
halfSample,
},
decoder: {
readers: ['code_93_reader'],
},
}), code93TestSet);
});

View File

@@ -0,0 +1,17 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('EAN Decoder Tests', () => {
const eanTestSet = [
{ 'name': 'image-001.jpg', 'result': '3574660239843', format: 'ean_13' },
{ 'name': 'image-002.jpg', 'result': '8032754490297', format: 'ean_13' },
{ 'name': 'image-004.jpg', 'result': '9002233139084', format: 'ean_13' },
{ 'name': 'image-003.jpg', 'result': '4006209700068', format: 'ean_13' },
{ 'name': 'image-005.jpg', 'result': '8004030044005', format: 'ean_13' },
{ 'name': 'image-006.jpg', 'result': '4003626011159', format: 'ean_13' },
{ 'name': 'image-007.jpg', 'result': '2111220009686', format: 'ean_13' },
{ 'name': 'image-008.jpg', 'result': '9000275609022', format: 'ean_13' },
{ 'name': 'image-009.jpg', 'result': '9004593978587', format: 'ean_13' },
{ 'name': 'image-010.jpg', 'result': '9002244845578', format: 'ean_13' },
];
runDecoderTestBothHalfSample('ean', (halfSample) => generateConfig({ locator: { halfSample } }), eanTestSet);
});

View File

@@ -0,0 +1,25 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('EAN-8 Decoder Tests', () => {
const ean8TestSet = [
{ 'name': 'image-001.jpg', 'result': '42191605', format: 'ean_8' },
{ 'name': 'image-002.jpg', 'result': '42191605', format: 'ean_8' },
{ 'name': 'image-003.jpg', 'result': '90311208', format: 'ean_8', allowFailInBrowser: true },
{ 'name': 'image-004.jpg', 'result': '24057257', format: 'ean_8' },
// {"name": "image-005.jpg", "result": "90162602"},
{ 'name': 'image-006.jpg', 'result': '24036153', format: 'ean_8' },
// {"name": "image-007.jpg", "result": "42176817"},
{ 'name': 'image-008.jpg', 'result': '42191605', format: 'ean_8' },
{ 'name': 'image-009.jpg', 'result': '42242215', format: 'ean_8', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-010.jpg', 'result': '42184799', format: 'ean_8' },
];
runDecoderTestBothHalfSample('ean_8', (halfSample) => generateConfig({
locator: {
patchSize: 'large',
halfSample,
},
decoder: {
readers: ['ean_8_reader']
}
}), ean8TestSet);
});

View File

@@ -0,0 +1,39 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('EAN Extended Decoder Tests', () => {
// Note: The main result's format is 'ean_13' (the parent barcode format).
// The supplement's format is available in result.codeResult.supplement.format
// and will correctly be 'ean_2' or 'ean_5' depending on the supplement type.
const eanExtendedTestSet = [
{ 'name': 'image-001.jpg', 'result': '900437801102701', format: 'ean_13', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-002.jpg', 'result': '419871600890101', format: 'ean_13' },
{ 'name': 'image-003.jpg', 'result': '419871600890101', format: 'ean_13', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-004.jpg', 'result': '978054466825652495', format: 'ean_13' },
{ 'name': 'image-005.jpg', 'result': '419664190890712', format: 'ean_13', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-006.jpg', 'result': '412056690699101', format: 'ean_13', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-007.jpg', 'result': '419204531290601', format: 'ean_13' },
{ 'name': 'image-008.jpg', 'result': '419871600890101', format: 'ean_13' },
{ 'name': 'image-009.jpg', 'result': '978054466825652495', format: 'ean_13' },
{ 'name': 'image-010.jpg', 'result': '900437801102701', format: 'ean_13' },
];
runDecoderTestBothHalfSample('ean_extended', (halfSample) => generateConfig({
inputStream: {
size: 800,
singleChannel: false,
},
locator: {
halfSample,
},
decoder: {
readers: [{
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader',
'ean_2_reader',
],
},
}],
},
}), eanExtendedTestSet);
});

View File

@@ -0,0 +1,70 @@
import Quagga from '../../../src/quagga';
import { expect } from 'chai';
import { it } from '../helpers';
describe('EAN Supplement Format Tests', () => {
// Test that verifies the supplement format is correctly returned as 'ean_2' or 'ean_5'
// rather than inheriting the parent 'ean_13' format
const isBrowser = typeof window !== 'undefined';
const fixturePrefix = isBrowser ? '/' : '';
const baseConfig = {
inputStream: {
size: 800,
singleChannel: false,
},
locator: {
patchSize: 'medium' as const,
halfSample: false,
},
numOfWorkers: 0,
decoder: {
readers: [{
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader',
'ean_2_reader',
],
},
}],
},
};
it('should return ean_2 format for 2-digit supplement', async function() {
this.timeout(30000);
const config = {
...baseConfig,
src: `${fixturePrefix}test/fixtures/ean_extended/image-002.jpg`, // EAN-13 with 2-digit supplement
};
const result = await Quagga.decodeSingle(config);
expect(result).to.be.an('Object');
expect(result.codeResult).to.be.an('Object');
expect(result.codeResult.format).to.equal('ean_13');
expect(result.codeResult.supplement).to.be.an('Object');
expect(result.codeResult.supplement.format).to.equal('ean_2');
expect(result.codeResult.supplement.code).to.equal('01');
});
it('should return ean_5 format for 5-digit supplement', async function() {
this.timeout(30000);
const config = {
...baseConfig,
src: `${fixturePrefix}test/fixtures/ean_extended/image-004.jpg`, // EAN-13 with 5-digit supplement
};
const result = await Quagga.decodeSingle(config);
expect(result).to.be.an('Object');
expect(result.codeResult).to.be.an('Object');
expect(result.codeResult.format).to.equal('ean_13');
expect(result.codeResult.supplement).to.be.an('Object');
expect(result.codeResult.supplement.format).to.equal('ean_5');
expect(result.codeResult.supplement.code).to.equal('52495');
});
});

View File

@@ -0,0 +1,21 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('Interleaved 2 of 5 Decoder Tests', () => {
const i2of5TestSet = [
{ 'name': 'image-001.jpg', 'result': '2167361334', format: 'i2of5' },
{ 'name': 'image-002.jpg', 'result': '2167361334', format: 'i2of5' },
{ 'name': 'image-003.jpg', 'result': '2167361334', format: 'i2of5' },
{ 'name': 'image-004.jpg', 'result': '2167361334', format: 'i2of5' },
{ 'name': 'image-005.jpg', 'result': '2167361334', format: 'i2of5' },
];
runDecoderTestBothHalfSample('i2of5', (halfSample) => generateConfig({
inputStream: { size: 1375, singleChannel: false }, // 1.25x scaling (from 1100x658) provides optimal detection
locator: {
patchSize: 'small',
halfSample,
},
decoder: {
readers: ['i2of5_reader'],
},
}), i2of5TestSet);
});

View File

@@ -0,0 +1,129 @@
import { runDecoderTestBothHalfSample, runNoCodeTest, generateConfig } from '../helpers';
describe('Pharmacode Decoder Tests', () => {
// Synthetic test images with known values
// Note: Tests only run with halfSample: false currently work reliably
// halfSample: true causes bar width detection issues for some images
const pharmacodeTestSet = [
{ 'name': 'image-001.jpg', 'result': '3', format: 'pharmacode' },
{ 'name': 'image-002.jpg', 'result': '7', format: 'pharmacode' },
{ 'name': 'image-003.jpg', 'result': '12', format: 'pharmacode' },
{ 'name': 'image-004.jpg', 'result': '15', format: 'pharmacode' },
{ 'name': 'image-005.jpg', 'result': '64', format: 'pharmacode' },
{ 'name': 'image-006.jpg', 'result': '100', format: 'pharmacode' },
{ 'name': 'image-007.jpg', 'result': '255', format: 'pharmacode' },
{ 'name': 'image-008.jpg', 'result': '755', format: 'pharmacode' },
{ 'name': 'image-009.jpg', 'result': '1000', format: 'pharmacode' },
{ 'name': 'image-010.jpg', 'result': '4096', format: 'pharmacode' },
{ 'name': 'image-011.jpg', 'result': '12345', format: 'pharmacode' },
{ 'name': 'image-012.jpg', 'result': '65535', format: 'pharmacode' },
];
// Real-world test images added by @ericblade
// Images 013, 014, 018 contain Pharmacode value 123456
// Image 017 contains multiple barcodes: 4174 and 3715
// Images 015, 016 have unknown values
const pharmacodeRealWorldPositiveTestSet = [
{ 'name': 'image-013.png', 'result': '123456', format: 'pharmacode' },
// image-014 is two-track pharmacode, not supported at the moment -- maybe not ever depending on difficulty level
// { 'name': 'image-014.png', 'result': '123456', format: 'pharmacode' },
];
// image-018 requires a constrained scan window to avoid false positives elsewhere in the frame
// still working out how to fix the false positive from the "orange and white" barcode.
const pharmacodeRealWorldAreaConstrainedTestSet = [
{ 'name': 'image-018.png', 'result': '123456', format: 'pharmacode' },
];
// Images intentionally expected to decode nothing (should succeed with empty result)
const pharmacodeRealWorldNoCodeTestSet = [
{ 'name': 'image-015.png', 'result': '', format: 'pharmacode' },
{ 'name': 'image-016.png', 'result': '', format: 'pharmacode' },
{ 'name': 'image-016-sheared.png', 'result': '', format: 'pharmacode' },
{ 'name': 'image-017.png', 'result': '', format: 'pharmacode' },
];
// Cross-barcode rejection: i2of5 images should be rejected by pharmacode reader
// This ensures the pharmacode reader doesn't accidentally decode other barcode types
const pharmacodeCrossBarcodeRejectionTestSet = [
{ 'name': 'image-011.jpg', 'result': '', format: 'pharmacode' },
];
// Use locate: false since test images are synthetically generated and pre-cropped to contain only the barcode (location detection not required)
runDecoderTestBothHalfSample('pharmacode set 1', (halfSample) => generateConfig({
locate: false,
locator: {
halfSample,
},
decoder: {
readers: ['pharmacode_reader']
}
}), pharmacodeTestSet, 'pharmacode');
runDecoderTestBothHalfSample('pharmacode set 2', (halfSample) => generateConfig({
locate: false,
inputStream: {
size: 800,
},
locator: {
halfSample,
patchSize: 'large',
},
decoder: {
readers: ['pharmacode_reader']
}
}), pharmacodeRealWorldPositiveTestSet, 'pharmacode');
// Dedicated run for image-018 with a narrowed search area (bottom 50%) - top 50% has an unreadable code
runDecoderTestBothHalfSample('pharmacode area constrained', (halfSample) => generateConfig({
locate: false,
inputStream: {
size: 800,
area: {
top: '50%',
},
},
locator: {
halfSample,
patchSize: 'large',
},
decoder: {
readers: ['pharmacode_reader']
}
}), pharmacodeRealWorldAreaConstrainedTestSet, 'pharmacode');
// Explicitly validate that certain images decode to nothing (empty barcodes array)
[true, false].forEach((halfSample) => {
runNoCodeTest(`pharmacode SHOULD NOT DECODE halfSample:${halfSample}`, generateConfig({
locate: false,
inputStream: {
size: 800,
},
locator: {
halfSample,
patchSize: 'large',
},
decoder: {
readers: ['pharmacode_reader']
}
}), pharmacodeRealWorldNoCodeTestSet, 'pharmacode');
});
// Cross-barcode rejection: Pharmacode reader should reject other barcode types (e.g., i2of5)
[true, false].forEach((halfSample) => {
runNoCodeTest(`pharmacode rejects i2of5 barcodes halfSample:${halfSample}`, generateConfig({
locate: false,
inputStream: {
size: 800,
},
locator: {
halfSample,
patchSize: 'large',
},
decoder: {
readers: ['pharmacode_reader']
}
}), pharmacodeCrossBarcodeRejectionTestSet, 'i2of5');
});
});

View File

@@ -0,0 +1,24 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('UPC-A Decoder Tests', () => {
const upcTestSet = [
{ 'name': 'image-001.jpg', 'result': '882428015268', format: 'upc_a' },
{ 'name': 'image-002.jpg', 'result': '882428015268', format: 'upc_a' },
{ 'name': 'image-003.jpg', 'result': '882428015084', format: 'upc_a' },
{ 'name': 'image-004.jpg', 'result': '882428015343', format: 'upc_a' },
{ 'name': 'image-005.jpg', 'result': '882428015343', format: 'upc_a' },
{ 'name': 'image-006.jpg', 'result': '882428015046', format: 'upc_a', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-007.jpg', 'result': '882428015084', format: 'upc_a' },
{ 'name': 'image-008.jpg', 'result': '882428015046', format: 'upc_a' },
{ 'name': 'image-009.jpg', 'result': '039047013551', format: 'upc_a' },
{ 'name': 'image-010.jpg', 'result': '039047013551', format: 'upc_a', allowFailInNode: true, allowFailInBrowser: true },
];
runDecoderTestBothHalfSample('upc', (halfSample) => generateConfig({
locator: {
halfSample,
},
decoder: {
readers: ['upc_reader']
}
}), upcTestSet);
});

View File

@@ -0,0 +1,24 @@
import { runDecoderTestBothHalfSample, generateConfig } from '../helpers';
describe('UPC-E Decoder Tests', () => {
const upcETestSet = [
{ 'name': 'image-001.jpg', 'result': '04965802', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-002.jpg', 'result': '04965802', format: 'upc_e' },
{ 'name': 'image-003.jpg', 'result': '03897425', format: 'upc_e' },
{ 'name': 'image-004.jpg', 'result': '05096893', format: 'upc_e' },
{ 'name': 'image-005.jpg', 'result': '05096893', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-006.jpg', 'result': '05096893', format: 'upc_e' },
{ 'name': 'image-007.jpg', 'result': '03897425', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-008.jpg', 'result': '01264904', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-009.jpg', 'result': '01264904', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-010.jpg', 'result': '01264904', format: 'upc_e', allowFailInNode: true, allowFailInBrowser: true },
];
runDecoderTestBothHalfSample('upc_e', (halfSample) => generateConfig({
locator: {
halfSample,
},
decoder: {
readers: ['upc_e_reader']
}
}), upcETestSet);
});

View File

@@ -0,0 +1,48 @@
import Quagga from '../../src/quagga';
import TestExternalCode128Reader from '../../src/reader/test_external_code_128_reader';
import { runDecoderTestBothHalfSample, generateConfig } from './helpers';
describe('External Reader Test, using test external code_128 reader', () => {
// NOTE: This test demonstrates the external reader plugin API.
// There is a known issue where external readers may fail intermittently in TypeScript
// test environments. The .allowFail mechanism handles this by skipping failing tests
// rather than failing the build. This issue does not occur in production (compiled code).
// External readers work correctly in production (compiled code).
describe('works', () => {
before(() => {
Quagga.registerReader('external_code_128_reader', TestExternalCode128Reader);
});
// Note: FNC1 characters are represented as ASCII 29 (Group Separator, \x1D or \u001d)
// These are used in GS1-128 barcodes as field separators
const FNC1 = String.fromCharCode(29);
const externalCode128TestSet = [
{ 'name': 'image-001.jpg', 'result': '0001285112001000040801', format: 'code_128' },
{ 'name': 'image-002.jpg', 'result': 'FANAVF14617104', format: 'code_128' },
{ 'name': 'image-003.jpg', 'result': '673023', format: 'code_128' },
{ 'name': 'image-004.jpg', 'result': '010210150301625334', format: 'code_128', allowFailInNode: true, allowFailInBrowser: true },
{ 'name': 'image-005.jpg', 'result': '419055603900009001012999', format: 'code_128' },
{ 'name': 'image-006.jpg', 'result': '419055603900009001012999', format: 'code_128' },
// GS1-128 barcode with FNC1 characters as field separators
{ 'name': 'image-007.jpg', 'result': `${FNC1}42095747${FNC1}9499907123456123456781`, format: 'code_128' },
{ 'name': 'image-008.jpg', 'result': '1020185021797280784055', format: 'code_128' },
{ 'name': 'image-009.jpg', 'result': '0001285112001000040801', format: 'code_128' },
{ 'name': 'image-010.jpg', 'result': '673023', format: 'code_128' },
// TODO: need to implement having different inputStream parameters to be able to
// read this one -- it works only with inputStream size set to 1600 presently, but
// other samples break at that high a size.
// { name: 'image-011.png', result: '33c64780-a9c0-e92a-820c-fae7011c11e2' },
];
runDecoderTestBothHalfSample('code_128_external', (halfSample) => generateConfig({
inputStream: {
size: 800,
singleChannel: false,
},
locator: {
halfSample,
},
decoder: {
readers: ['external_code_128_reader'],
},
}), externalCode128TestSet, 'code_128'); // Use code_128 fixture path
});
});

View File

@@ -0,0 +1,113 @@
import Quagga from '../../src/quagga';
import { QuaggaJSConfigObject } from '../../type-definitions/quagga';
import { expect } from 'chai';
import { it as mochaIt } from 'mocha';
// Export our own 'it' with allowFail support
export const it = Object.assign(
mochaIt,
{
allowFail: (title: string, callback: Function) => {
mochaIt(title, function() {
return Promise.resolve().then(() => {
return callback.apply(this, arguments);
}).catch((err) => {
console.trace('* error during test', title, err);
this.skip();
});
});
}
}
);
export function runDecoderTest(name: string, config: QuaggaJSConfigObject, testSet: Array<{ name: string, result: string, format: string, allowFailInNode?: boolean, allowFailInBrowser?: boolean }>, halfSampleLabel?: string, fixturePath?: string) {
const testLabel = halfSampleLabel ? `${name} (${halfSampleLabel})` : name;
const actualFixturePath = fixturePath || name;
describe(`Decoder ${testLabel}`, () => {
testSet.forEach((sample) => {
// Use the flags on the test item as the authoritative source
const isBrowser = typeof window !== 'undefined';
const shouldAllowFail = isBrowser ? sample.allowFailInBrowser : sample.allowFailInNode;
const testFn = shouldAllowFail ? it.allowFail : it;
testFn(`decodes ${sample.name}`, async function() {
this.timeout(20000); // need to set a long timeout because laptops sometimes lag like hell in tests when they go low power
const thisConfig = {
...config,
src: `${isBrowser ? '/' : ''}test/fixtures/${actualFixturePath}/${sample.name}`,
};
const result = await Quagga.decodeSingle(thisConfig);
// // console.warn(`* Expect result ${JSON.stringify(result)} to be an object`);
expect(result).to.be.an('Object');
expect(result.codeResult).to.be.an('Object');
expect(result.codeResult.code).to.equal(sample.result);
expect(result.codeResult.format).to.equal(sample.format);
expect(Quagga.canvas).to.be.an('Object');
expect(Quagga.canvas.dom).to.be.an('Object');
expect(Quagga.canvas.ctx).to.be.an('Object');
});
});
});
}
// Helper function to run decoder tests with both halfSample configurations
export function runDecoderTestBothHalfSample(
name: string,
configGenerator: (halfSample: boolean) => QuaggaJSConfigObject,
testSet: Array<{ name: string, result: string, format: string, allowFailInNode?: boolean, allowFailInBrowser?: boolean }>,
fixturePath?: string
) {
describe(`Decoder ${name} (both halfSample configurations)`, () => {
runDecoderTest(name, configGenerator(true), testSet, 'halfSample: true', fixturePath);
runDecoderTest(name, configGenerator(false), testSet, 'halfSample: false', fixturePath);
});
}
// run test that should not fail but no barcode is in the images
export function runNoCodeTest(name: string, config: QuaggaJSConfigObject, testSet: Array<{ name: string, result: string, format: string }>, fixturePath?: string) {
const actualFixturePath = fixturePath || name;
describe(`Not decoding ${name}`, () => {
testSet.forEach((sample) => {
it(`should run without error (${sample.name})`, async function() {
this.timeout(20000); // need to set a long timeout because laptops sometimes lag like hell in tests when they go low power
const thisConfig = {
...config,
src: `${typeof window !== 'undefined' ? '/' : ''}test/fixtures/${actualFixturePath}/${sample.name}`,
};
const result = await Quagga.decodeSingle(thisConfig);
expect(result).to.be.an('Object');
// When multiple: false and no decode found, result should have codeResult.code as null or undefined
if (result.codeResult) {
expect(result.codeResult.code).to.be.null;
}
// // console.warn(`* Expect result ${JSON.stringify(result)} to be an object`);
expect(Quagga.canvas).to.be.an('Object');
expect(Quagga.canvas.dom).to.be.an('Object');
expect(Quagga.canvas.ctx).to.be.an('Object');
});
});
});
}
export function generateConfig(configOverride: QuaggaJSConfigObject = {}) {
const config: QuaggaJSConfigObject = {
inputStream: {
size: 640,
...configOverride.inputStream,
},
locator: {
patchSize: 'medium',
halfSample: true,
...configOverride.locator,
},
numOfWorkers: 0,
decoder: {
readers: ['ean_reader'],
...configOverride.decoder,
},
locate: configOverride.locate,
src: null,
};
return config;
}

View File

@@ -0,0 +1,74 @@
// TODO: write a test that ensures that Quagga.decodeSingle returns a Promise when it should
// TODO: write a test that tests the multiple: true decoding option, allowing for multiple barcodes in
// a single image to be returned.
// TODO: write a test that allows for locate: false and locator configs to be tested.
import Quagga from '../../src/quagga';
import { expect } from 'chai';
import { runNoCodeTest, generateConfig } from './helpers';
describe('Parallel decoding works', () => {
it('decodeSingle running in parallel', async () => {
// TODO: we should throw in some other formats here too.
const testSet = [
{ 'name': 'image-001.jpg', 'result': '3574660239843', format: 'ean_13' },
{ 'name': 'image-002.jpg', 'result': '8032754490297', format: 'ean_13' },
{ 'name': 'image-004.jpg', 'result': '9002233139084', format: 'ean_13' },
{ 'name': 'image-003.jpg', 'result': '4006209700068', format: 'ean_13' },
{ 'name': 'image-005.jpg', 'result': '8004030044005', format: 'ean_13' },
{ 'name': 'image-006.jpg', 'result': '4003626011159', format: 'ean_13' },
{ 'name': 'image-007.jpg', 'result': '2111220009686', format: 'ean_13' },
{ 'name': 'image-008.jpg', 'result': '9000275609022', format: 'ean_13' },
{ 'name': 'image-009.jpg', 'result': '9004593978587', format: 'ean_13' },
{ 'name': 'image-010.jpg', 'result': '9002244845578', format: 'ean_13' },
];
const promises: Array<Promise<any>> = [];
testSet.forEach(sample => {
const config = generateConfig();
config.src = `${typeof window !== 'undefined' ? '/' : ''}test/fixtures/ean/${sample.name}`;
promises.push(Quagga.decodeSingle(config));
});
const results = await Promise.all(promises).catch((err) => { console.warn('* error decoding simultaneously', err); throw(err); });
const testResults = testSet.map(x => x.result);
results.forEach((r, index) => {
expect(r).to.be.an('object');
expect(r.codeResult).to.be.an('object');
expect(r.codeResult.code).to.equal(testResults[index]);
});
});
});
describe('Canvas Update Test, avoid DOMException', () => {
// This test ensures that Quagga handles edge cases with invalid canvas dimensions
// (NaN width/height) without throwing a DOMException during canvas operations.
// This is a regression test - the library should gracefully handle invalid dimensions
// and return an empty result rather than crashing.
describe('works', () => {
runNoCodeTest(
'no_code',
generateConfig({
decoder: {
readers: ['code_128_reader', 'ean_reader'],
},
inputStream: {
constraints: {
width: NaN,
height: NaN
},
singleChannel: false,
},
locate: false,
locator: {
halfSample: true,
patchSize: 'x-large'
}
}),
[
{ 'name': 'image-001.jpg', 'result': null, format: 'code_128' },
]
);
});
});

View File

@@ -0,0 +1,90 @@
/**
* Tests to verify that barcode readers are processed in the order specified
* in the configuration. This is important for predictable decoding behavior.
*
* Key findings about reader order:
* 1. Internal readers (code_128_reader, ean_reader, etc.) are processed in the
* order they appear in the `readers` config array
* 2. External readers must be registered via `Quagga.registerReader()` BEFORE
* they can be used in the `readers` array
* 3. The position of a reader in the `readers` array determines when it attempts
* to decode (earlier = higher priority)
* 4. The first reader to successfully decode the barcode wins
*/
import Quagga from '../../src/quagga';
import { expect } from 'chai';
/**
* Helper function to construct fixture paths consistently across browser and Node environments
*/
function getFixturePath(folder: string, filename: string): string {
const prefix = typeof window !== 'undefined' ? '/' : '';
return `${prefix}test/fixtures/${folder}/${filename}`;
}
describe('Priority Behavior with Multiple Readers', () => {
it('should decode EAN-8 as ean_8 when ean_8_reader is prioritized over ean_reader', async function() {
this.timeout(20000);
// This test uses an EAN-8 barcode image (8 digits)
// When ean_8_reader is listed first, it should decode as ean_8
const config = {
inputStream: {
size: 640,
},
locator: {
patchSize: 'large',
halfSample: true,
},
numOfWorkers: 0,
decoder: {
// EAN-8 reader first - should decode EAN-8 barcodes as ean_8
readers: ['ean_8_reader', 'ean_reader'],
},
locate: true,
src: getFixturePath('ean_8', 'image-001.jpg'),
};
const result = await Quagga.decodeSingle(config);
expect(result).to.be.an('object');
expect(result.codeResult).to.be.an('object');
expect(result.codeResult.code).to.equal('42191605');
expect(result.codeResult.format).to.equal('ean_8');
});
it('should fallback to ean_8_reader when ean_reader cannot decode EAN-8 barcode', async function() {
this.timeout(20000);
// EAN-8 and EAN-13 are different barcode formats with different structures.
// The EAN-13 reader will not successfully decode an EAN-8 barcode,
// so it will return null and the decoder will try the next reader.
// This test demonstrates that reader order affects fallback behavior.
const config = {
inputStream: {
size: 640,
},
locator: {
patchSize: 'large',
halfSample: true,
},
numOfWorkers: 0,
decoder: {
// EAN-13 reader first, EAN-8 as fallback
readers: ['ean_reader', 'ean_8_reader'],
},
locate: true,
src: getFixturePath('ean_8', 'image-001.jpg'),
};
const result = await Quagga.decodeSingle(config);
// EAN-13 reader cannot decode EAN-8, so EAN-8 reader succeeds as fallback
expect(result).to.be.an('object');
expect(result.codeResult).to.be.an('object');
expect(result.codeResult.code).to.equal('42191605');
// Since EAN-13 reader fails, the EAN-8 reader handles it
expect(result.codeResult.format).to.equal('ean_8');
});
});