Skip to main content

limelight

 

A narration layer for Playwright that turns end-to-end tests into demos.

Overview

A limelight test is written once and runs in two modes. The silent mode is a plain headless e2e test, where each narration call is a no-op. The narrated mode runs the same test headed with an overlay of caption cards, spotlights, an animated cursor, screenshots, and video. A test body never branches on the mode. The Demo facade is the whole API.

Installation

Add limelight to your pyproject.toml:

[project.optional-dependencies]
development = [
    "playwright-limelight[django,pytest]",
]

Install it directly with pip instead:

pip install "playwright-limelight[django,pytest]"

The django extra installs the limelight.django adapter. The pytest extra installs pytest and pytest-playwright for the plugin.

Usage

Register the plugins in conftest.py. The first is Django-free; the second adds the live server and the page timeout a Django project needs:

pytest_plugins = ['limelight.pytest_plugin', 'limelight.django.pytest_plugin']

Write the demo as a test:

from limelight import Demo
from limelight.django import DjangoApplication


def test_order_approval_demo(page, live_server, admin_user):
    application = DjangoApplication(live_server=live_server)
    demo = Demo(page, application, name='order-approval', user=admin_user)

    demo.goto('home:dashboard')
    demo.title('Order Approval')
    demo.narrate('Open the order')
    demo.click(page.get_by_role('link', name='Orders'))

Run it silent, then narrated:

pytest -k order_approval_demo
DEMO_MODE=narrate pytest -k order_approval_demo --headed

Turn a narrated run into an mp4, a walkthrough, and subtitles:

DEMO_MODE=narrate DEMO_VIDEO=1 pytest -k order_approval_demo
limelight-render .demos/order-approval --title "Order Approval"

DEMO_VIDEO does not record the screen. The browser runs headless under Chrome's begin-frame control, and limelight advances the compositor one frame at a time, screenshotting each one and piping it into ffmpeg. Every hold, glide and transition is measured in frames rather than wall time, so video.mp4 comes out with one distinct frame per interval no matter how slow the machine is. It needs ffmpeg on the PATH.

DEMO_VIDEO_QUALITY picks what that costs:

Quality Output Use
low 1920x1080, 24 fps Iterating on pacing, where only the timing is being read back.
medium 1920x1080, 30 fps The default. Sharp enough to read the interface, and the cheapest of the three to produce.
high 3840x2160, 60 fps A released demo, where the render is done once and the file size does not matter.

The cost is carried by the frame count and the pixel count together. Each frame is one round trip to the compositor, so high asks for twice as many as medium, and each of those frames covers four times the pixels at a device scale factor of 2. A high render therefore takes around eight times the work of a medium one and leaves a file several times larger.

Page objects

A Scene is one screen of the application. The subclass names the route, proves the screen has loaded, and offers one method per action a viewer would take, so a test reads as a list of steps rather than a list of selectors.

from limelight import Scene
from playwright.sync_api import expect


class OrderScene(Scene):
    route = 'sales:order:page:list'

    def expect_ready(self) -> None:
        expect(self.search_field).to_be_visible()

    @property
    def search_field(self):
        return self.demo.page.get_by_placeholder('Search orders')

    def search(self, text: str) -> None:
        self._teach_focus(self.search_field, headline='Search the orders', label='Search')

        self._fill(self.search_field, text)

The protected helpers are the vocabulary a scene is written in. _click, _fill, _select, _check, _press, and _hover each perform the action through the demo and then hold, so a narrated run shows the drawn pointer land before the next step begins and a silent run pays nothing for the hold. _tab opens a tab by its name. _teach_focus waits for an element, narrates it, and spotlights it in one step, which makes it a barrier in silent mode as well as a caption in a narrated one; _teach_click does the same and then clicks.

A scene never calls a Playwright locator method directly, because a bare click moves the real mouse without moving the drawn one, and the click lands with the pointer somewhere else on screen.

Components

A widget that appears on many screens is a component rather than a method on each scene. Each one carries its selectors as class attributes, so markup that differs is a subclass overriding a selector rather than a fork of the driver.

Component What it drives
Modal A dialog: opening it from its control, filling it by label, and submitting it.
Dropdown A menu that opens from a trigger and lists its actions.
Confirm The inline prompt that stands between an action and its effect.
SearchAndSelect A dropdown field that filters a long list and picks one choice.
Navigator A navigation menu, addressed by the text a viewer reads on its links.
from limelight import Confirm, Dropdown


class RowMenu(Dropdown):
    trigger_selector = '.bi-three-dots-vertical'


class DestructiveConfirm(Confirm):
    button_selector = '.btn-danger'


RowMenu(demo, row).choose('Delete')
DestructiveConfirm(demo).accept('Delete')

Dropdown scopes its trigger, its menu, and its actions to the region it is given, so a page holding one menu per row can address a single row without a menu left open elsewhere answering for it. Confirm leaves its click unbarriered on purpose, because a prompt sits in front of anything from a form post to a background write, so the caller wraps the accept in the barrier that proves its own effect landed.

Barriers

Silent mode removes every time cushion, so correctness rests on retrying barriers rather than on holds. trigger_until_navigation, trigger_until_response, and trigger_until_visible each repeat a trigger until its effect arrives, because a click can land before the handler that listens for it is bound and a lost click leaves the page where it was with no error to catch. trigger_until_response matches by a substring of the response URL, by the method its request was made with, or by a predicate, exactly one at a time.

trigger_until_response(page, lambda: demo.click(submit), method='POST')

Demo.follow is the barriered form of clicking a link: it spotlights the link, clicks it with the drawn pointer, and waits out the page it leads to.

Configuration

Variable Description Default
DEMO_MODE The narration and viewport policy, one of silent, narrate, or present. silent
DEMO_SPEED The playback speed the demo starts at, one of normal, fast, faster, or turbo. normal
DEMO_STEP_MS The base hold length in milliseconds. 4500
DEMO_SHOTS Whether a narrated run writes screenshots. off
DEMO_VIDEO Whether a narrated run renders video.mp4 frame by frame in a headless browser. off
DEMO_VIDEO_QUALITY The resolution, frame rate and encoder settings, one of low, medium, or high. medium
DEMO_CURSOR_HIDDEN Whether the drawn pointer is left off a narrated run. off

Release files for playwright-limelight 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for playwright-limelight 0.2.2
File Size Uploaded
playwright_limelight-0.2.2.tar.gz 72.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for playwright-limelight 0.2.2
File Interpreter ABI Platform
playwright_limelight-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 171.8 kB

Release files / playwright_limelight-0.2.2.tar.gz

Download URL playwright_limelight-0.2.2.tar.gz
Size 72.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a42b33241f2cdc9b6a92829b99328da5986aec4bdf938929a32401a8a2f7319d
BLAKE2b-256 checksum
How to use checksums
8d8e78271e3d0e072737f8b80a665d2af6dd6817f747ac4e0d457f285d82f5c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / playwright_limelight-0.2.2-py3-none-any.whl

Download URL playwright_limelight-0.2.2-py3-none-any.whl
Size 99.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
36dc31a33969ecf568be80fd22b425d845cbb982e1b45d44a52dfd1a6d2e9e92
BLAKE2b-256 checksum
How to use checksums
d33806e7936b37d325965d5e97950163b403fbbff5d7582b67658bdd850ec1ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page