npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

scriptable-testlab

v0.4.10

Published

A lightweight, efficient tool designed to manage and update scripts for Scriptable.

Readme

scriptable-testlab

A comprehensive testing framework for simulating the Scriptable iOS app runtime environment

npm version License Node.js Version

Introduction

scriptable-testlab is a testing framework specifically designed for the Scriptable iOS app. It provides a complete mock runtime environment that enables developers to write and run unit tests without depending on physical iOS devices. The framework strictly implements types defined in @types/scriptable-ios to ensure type safety and precise API simulation.

Core Features

  • 🔄 Complete Scriptable API simulation with strict type safety
  • 🧪 Comprehensive mock implementations for all Scriptable modules
  • 📱 Device-independent testing environment with runtime simulation
  • 🔍 Type-safe development experience with full TypeScript support
  • ⚡ Fast and lightweight runtime based on scriptable-abstract
  • 🛠️ Seamless Jest integration with full testing utilities
  • 📊 Built-in test coverage support and mock assertions
  • 🔌 Modular architecture with pluggable components

Implementation Status

Current implementation covers:

  • ✅ Core Runtime Environment
  • ✅ Device and System APIs
  • ✅ UI Components and Widgets
  • ✅ Network and Data Operations
  • ✅ File System Operations
  • ✅ Calendar and Reminders
  • ✅ Media and Images
  • ✅ Security and Keychain
  • ✅ Location Services
  • ✅ Notifications

Installation

# Using npm
npm install --save-dev scriptable-testlab

# Using yarn
yarn add -D scriptable-testlab

# Using pnpm (recommended)
pnpm add -D scriptable-testlab

Basic Usage

Runtime Setup

import {runtime} from 'scriptable-testlab';

describe('Runtime Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    runtime.configure({
      device: {
        appearance: {
          isUsingDarkAppearance: true,
        },
      },
    });
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle system settings', () => {
    expect(Device.isUsingDarkAppearance()).toBe(true);
  });
});

Device Configuration

import {runtime} from 'scriptable-testlab';

describe('Device Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    runtime.configure({
      device: {
        model: 'iPhone',
        systemVersion: '16.0',
        appearance: {
          isUsingDarkAppearance: true,
        },
      },
    });
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should configure device settings', () => {
    expect(Device.model()).toBe('iPhone');
    expect(Device.systemVersion()).toBe('16.0');
    expect(Device.isUsingDarkAppearance()).toBe(true);
  });
});

Network Operations

import {runtime} from 'scriptable-testlab';

describe('Network Tests', () => {
  let request: Request;

  beforeEach(() => {
    runtime.setupMocks();
    request = new Request('https://api.example.com');
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle basic GET request', async () => {
    const mockedRequest = request as MockedObject<MockRequest>;
    mockedRequest.loadJSON.mockImplementation(async () => {
      mockedRequest.setState({
        response: {
          statusCode: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({message: 'Success', data: {id: 1}}),
        },
      });
    });
    const _response = await request.loadJSON();
    expect(request.response?.statusCode).toBe(200);
    expect(request.response?.headers['Content-Type']).toBe('application/json');
    expect(request.response?.body).toBe(JSON.stringify({message: 'Success', data: {id: 1}}));
  });

  test('should handle POST request with custom headers', async () => {
    const mockedRequest = request as MockedObject<MockRequest>;
    request.method = 'POST';
    request.headers = {
      'Content-Type': 'application/json',
    };
    request.body = JSON.stringify({data: 'test'});

    mockedRequest.loadJSON.mockImplementation(async () => {
      mockedRequest.setState({
        response: {
          statusCode: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({success: true}),
        },
      });
    });

    const _response = await request.loadJSON();
    expect(request.response?.statusCode).toBe(200);
    expect(request.response?.headers['Content-Type']).toBe('application/json');
    expect(request.response?.body).toBe(JSON.stringify({success: true}));
  });

  test('should handle different response types', async () => {
    const mockedRequest = request as MockedObject<MockRequest>;

    // String response
    mockedRequest.loadString.mockImplementation(async () => {
      mockedRequest.setState({
        response: {
          statusCode: 200,
          headers: {
            'Content-Type': 'text/plain',
          },
          body: 'Hello World',
        },
      });
      return 'Hello World';
    });

    const textResponse = await request.loadString();
    expect(typeof textResponse).toBe('string');
    expect(textResponse).toBe('Hello World');
    expect(request.response?.headers['Content-Type']).toBe('text/plain');

    // JSON response
    mockedRequest.loadJSON.mockImplementation(async () => {
      mockedRequest.setState({
        response: {
          statusCode: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({message: 'Hello'}),
        },
      });
      return {message: 'Hello'};
    });

    const jsonResponse = await request.loadJSON();
    expect(typeof jsonResponse).toBe('object');
    expect(jsonResponse).toEqual({message: 'Hello'});
    expect(request.response?.headers['Content-Type']).toBe('application/json');
  });

  test('should handle request errors', async () => {
    const mockedRequest = request as MockedObject<MockRequest>;
    request.url = 'invalid-url';

    mockedRequest.load.mockRejectedValue(new Error('Invalid URL'));
    await expect(request.load()).rejects.toThrow('Invalid URL');
  });
});

Location Services

import {runtime} from 'scriptable-testlab';

describe('Location Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    runtime.configure({
      location: {
        latitude: 37.7749,
        longitude: -122.4194,
        altitude: 0,
        horizontalAccuracy: 10,
        verticalAccuracy: 10,
      },
    });
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle location services', async () => {
    const location = runtime.location;
    expect(location.latitude).toBe(37.7749);
    expect(location.longitude).toBe(-122.4194);
    expect(location.altitude).toBe(0);
  });
});

File System Operations

import {runtime} from 'scriptable-testlab';

describe('FileSystem Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    const fm = FileManager.local();
    fm.writeString('test.txt', 'Hello World');
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle file operations', () => {
    const fm = FileManager.local();
    expect(fm.readString('test.txt')).toBe('Hello World');
    expect(fm.fileExists('test.txt')).toBe(true);

    // Write new content
    fm.writeString('test.txt', 'Updated content');
    expect(fm.readString('test.txt')).toBe('Updated content');
  });
});

Notification Testing

import {runtime} from 'scriptable-testlab';

describe('Notification Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    MockNotification.reset();
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle notifications', async () => {
    const notification = new Notification();
    notification.title = 'Test Title';
    notification.body = 'Test Body';
    notification.subtitle = 'Test Subtitle';

    await notification.schedule();

    const pending = await Notification.allPending();
    expect(pending).toHaveLength(1);
    expect(pending[0].title).toBe('Test Title');
    expect(pending[0].body).toBe('Test Body');
    expect(pending[0].subtitle).toBe('Test Subtitle');
  });
});

Calendar Integration

import {runtime} from 'scriptable-testlab';

describe('Calendar Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    MockCalendar.clearAll();
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle calendar operations', async () => {
    // Create a calendar
    const calendars = await Calendar.forEvents();
    const calendar = calendars[0];

    // Create an event
    const event = new CalendarEvent();
    event.title = 'Test Event';
    event.notes = 'Event description';
    event.startDate = new Date('2024-01-01T10:00:00');
    event.endDate = new Date('2024-01-01T11:00:00');
    event.isAllDay = false;

    // Verify the event properties
    expect(event.title).toBe('Test Event');
    expect(event.notes).toBe('Event description');
    expect(event.isAllDay).toBe(false);
  });
});

Script Global Object

import {runtime} from 'scriptable-testlab';

describe('Script Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle script operations', () => {
    // Set script name
    runtime.script.setState({name: 'test-script'});
    expect(Script.name()).toBe('test-script');

    // Set shortcut output
    const shortcutData = {result: 'success', value: 42};
    Script.setShortcutOutput(shortcutData);
    expect(runtime.script.state.shortcutOutput).toEqual(shortcutData);

    // Set widget
    const widget = new ListWidget();
    widget.addText('Hello World');
    Script.setWidget(widget);
    expect(runtime.script.state.widget).toBe(widget);

    // Complete script
    expect(() => Script.complete()).not.toThrow();
  });

  test('should handle script in different contexts', () => {
    // Configure script to run in widget
    runtime.configure({
      widget: {
        widgetFamily: 'medium',
        runsInWidget: true,
      },
    });
    expect(config.runsInWidget).toBe(true);
    expect(config.widgetFamily).toBe('medium');

    // Configure script to run with Siri
    runtime.configure({
      widget: {
        runsWithSiri: true,
      },
    });
    expect(config.runsWithSiri).toBe(true);

    // Configure script to run from home screen
    runtime.configure({
      widget: {
        runsFromHomeScreen: true,
      },
    });
    expect(config.runsFromHomeScreen).toBe(true);
  });
});

Pasteboard Operations

import {runtime} from 'scriptable-testlab';

describe('Pasteboard Tests', () => {
  let mockedPasteboard: MockedObject<MockPasteboard>;

  beforeEach(() => {
    runtime.setupMocks();
    mockedPasteboard = Pasteboard as MockedObject<MockPasteboard>;
    mockedPasteboard.clear();
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should handle text operations', () => {
    // Copy and paste text
    const text = 'Hello Scriptable';
    Pasteboard.copy(text);
    expect(Pasteboard.paste()).toBe(text);

    // Copy and paste string
    const str = 'Test String';
    Pasteboard.copyString(str);
    expect(Pasteboard.pasteString()).toBe(str);
  });

  test('should handle image operations', () => {
    // Create a test image
    const image = new MockImage();

    // Copy and paste image
    Pasteboard.copyImage(image);
    expect(Pasteboard.pasteImage()).toBe(image);
    expect(mockedPasteboard.hasImages()).toBe(true);
  });

  test('should handle multiple items', () => {
    const url = 'https://example.com';
    const text = 'Example Text';

    // Set multiple items
    mockedPasteboard.setItems([{text}, {url}]);

    // Verify items
    expect(Pasteboard.paste()).toBe(text);
    expect(mockedPasteboard.hasURLs()).toBe(true);
    expect(mockedPasteboard.getURLs()).toContain(url);
  });
});

Safari Operations

import {runtime} from 'scriptable-testlab';

describe('Safari Tests', () => {
  let mockedSafari: MockedObject<MockSafari>;

  beforeEach(() => {
    runtime.setupMocks();
    mockedSafari = Safari as MockedObject<MockSafari>;
    mockedSafari.setState({
      currentURL: null,
      inBackground: false,
      openMethod: null,
      fullscreen: false,
    });
  });

  afterEach(() => {
    runtime.clearMocks();
  });

  test('should demonstrate basic Safari operations', () => {
    // Example 1: Open the URL in your browser
    const browserUrl = 'https://example.com';
    Safari.open(browserUrl);
    expect(mockedSafari['state']).toEqual({
      currentURL: browserUrl,
      inBackground: false,
      openMethod: 'browser',
      fullscreen: false,
    });

    // Example 2: Open URL in app (full screen)
    const appUrl = 'https://example.com/app';
    Safari.openInApp(appUrl, true);
    expect(mockedSafari['state']).toEqual({
      currentURL: appUrl,
      inBackground: false,
      openMethod: 'app',
      fullscreen: true,
    });

    // Example 3: Open URL in app (not full screen)
    const appUrl2 = 'https://example.com/app2';
    Safari.openInApp(appUrl2);
    expect(mockedSafari['state']).toEqual({
      currentURL: appUrl2,
      inBackground: false,
      openMethod: 'app',
      fullscreen: false,
    });
  });

  test('should demonstrate error handling', () => {
    // Example 4: Handling invalid URLs
    expect(() => Safari.open('invalid-url')).toThrow('Invalid URL');

    // Example 5: Handling an invalid URL protocol
    expect(() => Safari.open('ftp://example.com')).toThrow('Invalid URL scheme');
  });

  test('should demonstrate URL validation', () => {
    // Example 6: Verify a valid URL
    const validUrls = ['http://example.com', 'https://example.com', 'https://sub.domain.com/path?query=1'];

    validUrls.forEach(url => {
      expect(() => Safari.open(url)).not.toThrow();
      expect(Safari.openInApp(url)).resolves.not.toThrow();
    });
  });
});

Console and Logging

import {runtime} from 'scriptable-testlab';

describe('Console Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    jest.spyOn(console, 'log');
    jest.spyOn(console, 'warn');
    jest.spyOn(console, 'error');
  });

  afterEach(() => {
    runtime.clearMocks();
    jest.restoreAllMocks();
  });

  test('should handle different log levels', () => {
    // Standard log
    console.log('Info message');
    expect(console.log).toHaveBeenCalledWith('Info message');

    // Warning log
    console.warn('Warning message');
    expect(console.warn).toHaveBeenCalledWith('Warning message');

    // Error log
    console.error('Error message');
    expect(console.error).toHaveBeenCalledWith('Error message');
  });

  test('should handle object logging', () => {
    const obj = {key: 'value'};
    console.log(JSON.stringify(obj));
    expect(console.log).toHaveBeenCalledWith(JSON.stringify(obj));
  });
});

Module Import

import {runtime} from 'scriptable-testlab';

describe('Module Import Tests', () => {
  beforeEach(() => {
    runtime.setupMocks();
    jest.resetModules();

    // Create mock modules
    jest.mock(
      'test-module',
      () => ({
        testFunction: () => 'test',
      }),
      {virtual: true},
    );

    jest.mock(
      'relative/path/module',
      () => ({
        relativeFunction: () => 'relative',
      }),
      {virtual: true},
    );

    jest.mock(
      'invalid-module',
      () => {
        const error = new Error('Unexpected token');
        error.name = 'SyntaxError';
        throw error;
      },
      {virtual: true},
    );
  });

  test('should handle module imports', () => {
    const module = importModule('test-module');
    expect(module).toBeDefined();
    expect(typeof (module as {testFunction: () => string}).testFunction).toBe('function');
    expect((module as {testFunction: () => string}).testFunction()).toBe('test');

    const relativeModule = importModule('relative/path/module');
    expect(relativeModule).toBeDefined();
    expect(typeof (relativeModule as {relativeFunction: () => string}).relativeFunction).toBe('function');
    expect((relativeModule as {relativeFunction: () => string}).relativeFunction()).toBe('relative');
  });

  test('should handle module import errors', () => {
    expect(() => {
      importModule('non-existent-module');
    }).toThrow('Module not found: non-existent-module');

    expect(() => {
      importModule('invalid-module');
    }).toThrow('Syntax error in module: invalid-module');
  });
});

License

This project is licensed under the Apache-2.0 License - see the LICENSE file for details.