Unit Testing Plugin Wrappers

Most plugin code is a thin wrapper: build the options, call the plugin, decide what happens next. That logic tests in milliseconds, without a device, once the plugin module is replaced with spies. BookNest uses Vitest 4.0.18 102,989 (MIT, npm 2,036 install -D vitest jsdom 21,691 , as in the Ionic 20,753 starter of ionic start), with a vitest.config.js selecting the jsdom environment so show() finds a document. The tests pin down update() (Capawesome Live Update):

tests/unit/lab.test.js (its first 32 lines; three linkToRoute() cases follow)JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LiveUpdate } from '@capawesome/capacitor-live-update';
import { linkToRoute, update } from '../../src/js/lab.js';
// Replace the native plugin with spies: the test checks what the wrapper asks for
vi.mock('@capawesome/capacitor-live-update', () => ({
  LiveUpdate: {
    downloadBundle: vi.fn(async () => {}),
    setNextBundle: vi.fn(async () => {}),
    getBundles: vi.fn(async () => ({ bundleIds: ['1.1.0'] })),
    reload: vi.fn(async () => {}),
  },
}));
beforeEach(() => vi.clearAllMocks());
describe('update()', () => {
  it('downloads the bundle, makes it next and reloads', async () => {
    await update('1.1.0');
    expect(LiveUpdate.downloadBundle).toHaveBeenCalledWith(
      { url: 'http://10.0.2.2:8480/bundles/1.1.0.zip', bundleId: '1.1.0' });
    expect(LiveUpdate.setNextBundle).toHaveBeenCalledWith({ bundleId: '1.1.0' });
    expect(LiveUpdate.reload).toHaveBeenCalledOnce();
  });
  it('never reloads after a failed download', async () => {
    LiveUpdate.downloadBundle.mockRejectedValueOnce(new Error('CLEARTEXT not permitted'));
    await expect(update('1.1.0')).rejects.toThrow('CLEARTEXT');
    expect(LiveUpdate.setNextBundle).not.toHaveBeenCalled();
    expect(LiveUpdate.reload).not.toHaveBeenCalled();
  });
});

vi.mock() is hoisted above the imports, so lab.js gets the spies. npx vitest run --reporter=verbose:

Output of 116
 ✓ tests/unit/lab.test.js > update() > downloads the bundle, makes it next and reloads 44ms
 ✓ tests/unit/lab.test.js > update() > never reloads after a failed download 34ms
 ✓ tests/unit/lab.test.js > linkToRoute() > booknest://lab/ -> /lab/ 2ms
 ✓ tests/unit/lab.test.js > linkToRoute() > booknest://books/3/ -> /books/3/ 1ms
 ✓ tests/unit/lab.test.js > linkToRoute() > https://booknest.example.com/books/3/?tab=notes ->
   /books/3/?tab=notes 1ms
 Test Files  1 passed (1)
      Tests  5 passed (5)

The second test proves that a failed download never reloads the app into a missing bundle. For the native halves, the plugin generator of Scaffolding a Plugin adds verify:android (./gradlew clean build test, JUnit 48,319 ) and verify:ios (xcodebuild tests).