diff --git a/changelog.md b/changelog.md index 9dba05e..d499deb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-01-10 - 3.0.0 - BREAKING CHANGE(devicemanager) +migrate tests to new UniversalDevice/feature-based API, add device factories, SNMP protocol/feature and IP helper utilities + +- Replace protocol-specific device classes (Scanner, Printer) with UniversalDevice and feature objects (ScanFeature, PrintFeature, PlaybackFeature, VolumeFeature, PowerFeature, SnmpFeature) +- Add device factory functions: createScanner, createPrinter, createSpeaker, createUpsDevice +- Add DeviceManager.getDevices selector and updated selectDevice behavior (throws when no match) +- Expose SnmpProtocol and other protocol implementations +- Introduce IP helper utilities: isValidIp, cidrToIps, getLocalSubnet +- Update tests and logging to use feature-based APIs and factories (selectFeature/getFeature, hasFeature, featureCount) + ## 2026-01-09 - 2.3.1 - fix(readme) update README to comprehensive, TypeScript-first documentation covering installation, quick start, examples, API usage, events, error handling, requirements, credits, and legal/company information diff --git a/test/test.ts b/test/test.ts index 64b0122..39797d0 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,26 +1,40 @@ import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as devicemanager from '../ts/index.js'; -// Test imports +// Test core exports tap.test('should export DeviceManager', async () => { expect(devicemanager.DeviceManager).toBeDefined(); expect(typeof devicemanager.DeviceManager).toEqual('function'); }); -tap.test('should export Scanner', async () => { - expect(devicemanager.Scanner).toBeDefined(); - expect(typeof devicemanager.Scanner).toEqual('function'); +tap.test('should export UniversalDevice', async () => { + expect(devicemanager.UniversalDevice).toBeDefined(); + expect(typeof devicemanager.UniversalDevice).toEqual('function'); }); -tap.test('should export Printer', async () => { - expect(devicemanager.Printer).toBeDefined(); - expect(typeof devicemanager.Printer).toEqual('function'); +tap.test('should export Features', async () => { + expect(devicemanager.ScanFeature).toBeDefined(); + expect(devicemanager.PrintFeature).toBeDefined(); + expect(devicemanager.PlaybackFeature).toBeDefined(); + expect(devicemanager.VolumeFeature).toBeDefined(); + expect(devicemanager.PowerFeature).toBeDefined(); + expect(devicemanager.SnmpFeature).toBeDefined(); +}); + +tap.test('should export device factories', async () => { + expect(devicemanager.createScanner).toBeDefined(); + expect(devicemanager.createPrinter).toBeDefined(); + expect(devicemanager.createSpeaker).toBeDefined(); + expect(devicemanager.createUpsDevice).toBeDefined(); + expect(typeof devicemanager.createScanner).toEqual('function'); + expect(typeof devicemanager.createPrinter).toEqual('function'); }); tap.test('should export protocol implementations', async () => { expect(devicemanager.EsclProtocol).toBeDefined(); expect(devicemanager.SaneProtocol).toBeDefined(); expect(devicemanager.IppProtocol).toBeDefined(); + expect(devicemanager.SnmpProtocol).toBeDefined(); }); tap.test('should export retry helpers', async () => { @@ -39,6 +53,28 @@ tap.test('should create DeviceManager instance', async () => { expect(dm.isDiscovering).toEqual(false); expect(dm.getScanners()).toEqual([]); expect(dm.getPrinters()).toEqual([]); + expect(dm.getDevices()).toEqual([]); +}); + +// Test device selector +tap.test('should support device selection with IDeviceSelector', async () => { + const dm = new devicemanager.DeviceManager({ + autoDiscovery: false, + }); + + // Empty manager should return empty array + expect(dm.getDevices({ hasFeature: 'scan' })).toEqual([]); + expect(dm.getDevices({ name: 'Test' })).toEqual([]); + + // selectDevice should throw when no devices match + let error: Error | null = null; + try { + dm.selectDevice({ address: '192.168.1.100' }); + } catch (e) { + error = e as Error; + } + expect(error).not.toBeNull(); + expect(error?.message).toContain('No device found'); }); // Test retry helper @@ -114,17 +150,18 @@ tap.test('should start and stop discovery', async () => { // Wait a bit for potential device discovery await new Promise((resolve) => setTimeout(resolve, 2000)); - // Log discovered devices - const scanners = dm.getScanners(); - const printers = dm.getPrinters(); + // Log discovered devices using new API + const scanners = dm.getDevices({ hasFeature: 'scan' }); + const printers = dm.getDevices({ hasFeature: 'print' }); console.log(`Discovered ${scanners.length} scanner(s) and ${printers.length} printer(s)`); for (const scanner of scanners) { - console.log(` Scanner: ${scanner.name} (${scanner.address}:${scanner.port}) - ${scanner.protocol}`); + const scanFeature = scanner.getFeature('scan'); + console.log(` Scanner: ${scanner.name} (${scanner.address}) - ${scanFeature?.protocol || 'unknown'}`); } for (const printer of printers) { - console.log(` Printer: ${printer.name} (${printer.address}:${printer.port})`); + console.log(` Printer: ${printer.name} (${printer.address})`); } await dm.stopDiscovery(); @@ -134,9 +171,9 @@ tap.test('should start and stop discovery', async () => { await dm.shutdown(); }); -// Test Scanner creation from discovery info -tap.test('should create Scanner from discovery info', async () => { - const scanner = devicemanager.Scanner.fromDiscovery({ +// Test Scanner creation using factory +tap.test('should create Scanner device using factory', async () => { + const scanner = devicemanager.createScanner({ id: 'test:scanner:1', name: 'Test Scanner', address: '192.168.1.100', @@ -150,40 +187,75 @@ tap.test('should create Scanner from discovery info', async () => { }, }); + expect(scanner).toBeInstanceOf(devicemanager.UniversalDevice); expect(scanner.name).toEqual('Test Scanner'); expect(scanner.address).toEqual('192.168.1.100'); - expect(scanner.port).toEqual(443); - expect(scanner.protocol).toEqual('escl'); - expect(scanner.supportedFormats).toContain('jpeg'); - expect(scanner.supportedFormats).toContain('pdf'); - expect(scanner.supportedColorModes).toContain('color'); - expect(scanner.supportedColorModes).toContain('grayscale'); - expect(scanner.supportedSources).toContain('flatbed'); - expect(scanner.supportedSources).toContain('adf'); - expect(scanner.hasAdf).toEqual(true); + expect(scanner.hasFeature('scan')).toEqual(true); + + const scanFeature = scanner.selectFeature('scan'); + expect(scanFeature).toBeInstanceOf(devicemanager.ScanFeature); + expect(scanFeature.protocol).toEqual('escl'); }); -// Test Printer creation from discovery info -tap.test('should create Printer from discovery info', async () => { - const printer = devicemanager.Printer.fromDiscovery({ +// Test Printer creation using factory +tap.test('should create Printer device using factory', async () => { + const printer = devicemanager.createPrinter({ id: 'test:printer:1', name: 'Test Printer', address: '192.168.1.101', port: 631, + ippPath: '/ipp/print', txtRecords: { 'ty': 'Brother HL-L2350DW', - 'rp': 'ipp/print', 'Color': 'T', 'Duplex': 'T', }, }); + expect(printer).toBeInstanceOf(devicemanager.UniversalDevice); expect(printer.name).toEqual('Test Printer'); expect(printer.address).toEqual('192.168.1.101'); - expect(printer.port).toEqual(631); - expect(printer.supportsColor).toEqual(true); - expect(printer.supportsDuplex).toEqual(true); - expect(printer.uri).toContain('ipp://'); + expect(printer.hasFeature('print')).toEqual(true); + + const printFeature = printer.selectFeature('print'); + expect(printFeature).toBeInstanceOf(devicemanager.PrintFeature); +}); + +// Test UniversalDevice feature management +tap.test('should manage features on UniversalDevice', async () => { + const device = new devicemanager.UniversalDevice('192.168.1.50', 80, { + name: 'Test Device', + }); + + expect(device.name).toEqual('Test Device'); + expect(device.address).toEqual('192.168.1.50'); + expect(device.featureCount).toEqual(0); + expect(device.hasFeature('scan')).toEqual(false); + + // selectFeature should throw when feature doesn't exist + let error: Error | null = null; + try { + device.selectFeature('scan'); + } catch (e) { + error = e as Error; + } + expect(error).not.toBeNull(); + expect(error?.message).toContain("does not have feature 'scan'"); + + // getFeature should return undefined + expect(device.getFeature('scan')).toBeUndefined(); +}); + +// Test IP helpers +tap.test('should export IP helper utilities', async () => { + expect(devicemanager.isValidIp).toBeDefined(); + expect(devicemanager.cidrToIps).toBeDefined(); + expect(devicemanager.getLocalSubnet).toBeDefined(); + + // Test isValidIp + expect(devicemanager.isValidIp('192.168.1.1')).toEqual(true); + expect(devicemanager.isValidIp('invalid')).toEqual(false); + expect(devicemanager.isValidIp('256.1.1.1')).toEqual(false); }); export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index f480320..1a26208 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@ecobridge.xyz/devicemanager', - version: '2.3.1', + version: '3.0.0', description: 'a device manager for talking to devices on network and over usb' }