trmnlp-test 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/exe/trmnlp-test +53 -0
- data/lib/trmnlp_test/version.rb +6 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cfefb51546891c33222e11f7c2ba094168787d9207966a1addbc307462290fd2
|
|
4
|
+
data.tar.gz: 4323d3400e827f4d88913f1d624ccf7d64bd65e0f15428add123b8c3af01e782
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2b4b67e735b844dc2510eac564e98a825217516709ef67fd68e78294ffddb40bdc99075045e1390b09dde904e3bc2311b8974ffa1d70bf230f4ab6d36b1996a1
|
|
7
|
+
data.tar.gz: 6b590003ac56c97e22392b85376d3fcce7bb22d172e25636a02489ac9a0304b8c3d6ed83c7c8b9f5e8a06b2598379607b872e4bdf82085827686f617a78f9a67
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ExcuseMi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# trmnlp-test
|
|
2
|
+
|
|
3
|
+
Test framework for TRMNL plugins in the trmnlp format (`src/*.liquid`, `settings.yml`, `.trmnlp.yml`), in Docker.
|
|
4
|
+
|
|
5
|
+
- **Every data path:** webhook (`deep_merge`, `stream`, 2 kB / 5 kB limits), webhook + serverless (the transform runs when data arrives and its output is stored), polling (URLs, headers and body rendered with custom fields; JSON/XML), static, serverless transforms in Python, Ruby, Node and PHP, and `trmnl_state` carried between runs.
|
|
6
|
+
- **Same code as trmnlp:** trmnlp's own Liquid environment, settings parsing, polling parser and serverless wrapper. The image is rebuilt on every new trmnlp release (`:latest`, `:trmnlp-X.Y.Z`) once the self-tests pass on it.
|
|
7
|
+
- **Every device and setting:** every model from `/api/models`, their palettes from `/api/palettes`, portrait, real mashup slots, dark mode, themes, scale, text scale, fonts, and every framework version.
|
|
8
|
+
- **Deterministic:** frozen clock (transform via libfaketime, Liquid `now`, browser `Date`); HTTP(S) mocks for transforms (a MITM proxy, so they work with any library) and for polling and browser requests; cached framework assets and fonts.
|
|
9
|
+
- **Assertions on HTML or PNG:** Playwright locators on the rendered page, plus a picture reduced to the device palette for pixel checks, QR decoding, snapshots and the device image size limit.
|
|
10
|
+
- **Report:** a gallery `index.html` with every device picture, transform time and memory, and problems found, linked to the Playwright HTML report.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
gem install trmnlp-test # needs Ruby and Docker; runs ghcr.io/excusemi/trmnlp-test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Use
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
cd my-plugin
|
|
22
|
+
trmnlp-test init # trmnlp-test.config.js + test/trmnl/plugin.spec.js
|
|
23
|
+
trmnlp-test # run; Playwright args pass through (-g, --update-snapshots)
|
|
24
|
+
trmnlp-test models | versions # device models / framework versions
|
|
25
|
+
trmnlp-test update # pull the image now (otherwise at most once a day)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`--repo DIR` runs against another directory. `--mount DIR` mounts an extra directory.
|
|
29
|
+
|
|
30
|
+
**Parallel:** tests run in parallel workers, each with its own harness and browser (default: half the CPUs). Set the number with `workers: 4` (or `'50%'`) in the config, `TRMNLP_TEST_WORKERS=4`, or `--workers 4` / `-j 4`, in increasing priority. Separate runs can also go at the same time, for other plugins or the same one: the shared cache is safe for concurrent use. Give each run of the same repo its own `--report DIR`. The cache (assets, dependencies, mock CA) is kept in `~/.cache/trmnlp-test`.
|
|
31
|
+
|
|
32
|
+
From a clone, `bin/trmnlp-test` builds the image from source on the newest trmnlp (`TRMNLP_TEST_BUILD=1` rebuilds it; `TRMNLP_VERSION=x.y.z` picks another).
|
|
33
|
+
|
|
34
|
+
## Release
|
|
35
|
+
|
|
36
|
+
Pushing to `main`, and a daily check for a new trmnlp release, publish the image as `:latest` and `:trmnlp-X.Y.Z`. Pushing a tag `vX.Y.Z` (matching `lib/trmnlp_test/version.rb`) runs the self-tests, publishes `:X.Y.Z` and pushes the gem through rubygems.org trusted publishing (no stored key).
|
|
37
|
+
|
|
38
|
+
## Config: `trmnlp-test.config.js`
|
|
39
|
+
|
|
40
|
+
See [templates/trmnlp-test.config.js](templates/trmnlp-test.config.js). Transform dependencies:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
serverless: { dependencies: { python: ['icalendar==6.1.0'], ruby: 'Gemfile', node: ['date-fns@4'], php: ['nesbot/carbon:^3'] } }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`requests` (Python) and `httparty` (Ruby) are always installed, as on the hosted runtime. Each distinct list is installed once.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const { test, expect, matrix, VIEWS, DEVICES, FRAMEWORK } = require('trmnlp-test');
|
|
52
|
+
|
|
53
|
+
test('menu', async ({ trmnl }) => {
|
|
54
|
+
const screen = await trmnl.render({
|
|
55
|
+
device: 'og_plus', view: 'full', // any model, orientation: 'portrait', palette, darkMode, theme, scale, textScale, fonts, framework
|
|
56
|
+
fields: { data_source: 'webhook' }, // custom field values (defaults + .trmnlp.yml + these)
|
|
57
|
+
webhook: { merge_variables: {...} }, // or data: {...} to render given data as is
|
|
58
|
+
mocks: { 'https://api.example.com/*': { temp: 21 } },
|
|
59
|
+
transform: true, // false: skip the transform
|
|
60
|
+
now: '2026-10-02T08:00:00Z',
|
|
61
|
+
});
|
|
62
|
+
expect(screen).toRenderCleanly(); // Liquid, transform, page JS, assets, unmocked requests, screen size
|
|
63
|
+
await expect(screen.locator('[data-row]')).toHaveCount(7); // any Playwright locator assertion
|
|
64
|
+
await expect(screen).toHaveNoOverflow();
|
|
65
|
+
await expect(screen).toHaveNoOverlap('.title', 'img');
|
|
66
|
+
await expect(screen).toHaveQr('BCD\n002...');
|
|
67
|
+
await expect(screen).toMatchScreen(); // PNG snapshot (--update-snapshots)
|
|
68
|
+
await expect(screen).toFitDeviceImageLimit();
|
|
69
|
+
const png = await screen.png(); // pixel(x,y), colors(), inkRatio(rect), inkBounds(), crop()
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('transform only', async ({ trmnl }) => {
|
|
73
|
+
const run = await trmnl.transform({ mocks, fields });
|
|
74
|
+
expect(run).toTransformCleanly();
|
|
75
|
+
expect(run).toStayWithinServerlessLimits(); // 5 s, 128 MB
|
|
76
|
+
expect(run).toHaveRequested('https://api.example.com/x', { headers: { 'x-key': 'abc' } });
|
|
77
|
+
expect(run.output).toMatchObject({ ... });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a device over time', async ({ trmnl }) => {
|
|
81
|
+
const s = trmnl.session({ mocks });
|
|
82
|
+
await s.webhook({ merge_variables: {...}, merge_strategy: 'deep_merge' });
|
|
83
|
+
const screen = await s.render(); // stored webhook data + trmnl_state from the last run
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const s of matrix({ device: DEVICES.representative, view: VIEWS, darkMode: [false, true] })) {
|
|
87
|
+
test(`fits · ${s.label}`, async ({ trmnl }) => { /* trmnl.render(s) */ });
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Other helpers: `trmnl.plugin('other/dir')`, `trmnl.lint()` with `toPassLint({ allow })`, `FRAMEWORK.majors()`, `FRAMEWORK.latestOf('2')`.
|
|
92
|
+
|
|
93
|
+
## How it works
|
|
94
|
+
|
|
95
|
+
See [docs/README.md](docs/README.md) for diagrams of the components, a render, sessions and the file structure.
|
|
96
|
+
|
|
97
|
+
## Examples and self-tests
|
|
98
|
+
|
|
99
|
+
- `examples/payment-qr`: the payment QR plugin, current Liquid version and the earlier webhook + serverless version (53 tests).
|
|
100
|
+
- `test/self`: fixture plugins for every strategy and language (86 tests). Run with `bin/trmnlp-test run --root test/self`.
|
data/exe/trmnlp-test
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Runs trmnlp-test (a Docker image) against a TRMNL plugin repository.
|
|
5
|
+
# trmnlp-test [--repo DIR] [--mount DIR]... [run|init|models|versions|refresh|update|shell|version] [playwright args]
|
|
6
|
+
require 'etc'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative '../lib/trmnlp_test/version'
|
|
9
|
+
|
|
10
|
+
# :latest follows the newest trmnlp release; TRMNLP_TEST_IMAGE pins one (e.g. ...:trmnlp-0.13.1)
|
|
11
|
+
image = ENV.fetch('TRMNLP_TEST_IMAGE', "#{TrmnlpTest::IMAGE}:latest")
|
|
12
|
+
repo = Dir.pwd
|
|
13
|
+
mounts = []
|
|
14
|
+
args = ARGV.dup
|
|
15
|
+
while %w[--repo --mount].include?(args.first)
|
|
16
|
+
flag, dir = args.shift(2)
|
|
17
|
+
abort "trmnlp-test: #{flag} needs a directory" unless dir
|
|
18
|
+
path = File.realpath(dir)
|
|
19
|
+
flag == '--repo' ? repo = path : mounts += ['-v', "#{path}:#{path}"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
case args.first
|
|
23
|
+
when 'version'
|
|
24
|
+
puts "trmnlp-test #{TrmnlpTest::VERSION} (#{image})"
|
|
25
|
+
exit
|
|
26
|
+
when 'update'
|
|
27
|
+
exec 'docker', 'pull', image
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
abort 'trmnlp-test needs Docker: https://docs.docker.com/get-docker/' unless system('docker info', out: File::NULL, err: File::NULL)
|
|
31
|
+
|
|
32
|
+
cache = ENV.fetch('TRMNLP_TEST_CACHE_DIR', File.join(Dir.home, '.cache', 'trmnlp-test'))
|
|
33
|
+
FileUtils.mkdir_p(cache)
|
|
34
|
+
|
|
35
|
+
# a missing image is pulled; a :latest image is refreshed at most once a day
|
|
36
|
+
present = system('docker', 'image', 'inspect', image, out: File::NULL, err: File::NULL)
|
|
37
|
+
stamp = File.join(cache, '.pulled-' + image.gsub(/[^A-Za-z0-9.]+/, '_'))
|
|
38
|
+
stale = image.end_with?(':latest') && (!File.exist?(stamp) || Time.now - File.mtime(stamp) > 86_400)
|
|
39
|
+
if !present || (stale && !ENV['TRMNLP_TEST_NO_PULL'])
|
|
40
|
+
warn "trmnlp-test: pulling #{image}"
|
|
41
|
+
pulled = system('docker', 'pull', '-q', image, out: File::NULL)
|
|
42
|
+
abort("trmnlp-test: could not pull #{image}") if !pulled && !present
|
|
43
|
+
FileUtils.touch(stamp) if pulled
|
|
44
|
+
end
|
|
45
|
+
entry = []
|
|
46
|
+
if args.first == 'shell'
|
|
47
|
+
args.shift
|
|
48
|
+
entry = ['--entrypoint', 'bash']
|
|
49
|
+
end
|
|
50
|
+
tty = $stdout.tty? ? ['-t'] : []
|
|
51
|
+
env = %w[CI TRMNLP_TEST_DEBUG TRMNLP_TEST_WORKERS FORCE_COLOR].flat_map { |k| ['-e', k] }
|
|
52
|
+
exec 'docker', 'run', '--rm', '-i', *tty, '--init', '--ipc=host', '-u', "#{Process.uid}:#{Process.gid}",
|
|
53
|
+
'-v', "#{repo}:#{repo}", '-w', repo, '-v', "#{cache}:/cache", *mounts, *env, *entry, image, *args
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: trmnlp-test
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ExcuseMi
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Runs Playwright tests against TRMNL plugins in the trmnlp format: webhook,
|
|
13
|
+
polling, static and serverless transforms (Python, Ruby, Node, PHP) with mocked
|
|
14
|
+
HTTP and a frozen clock, on every TRMNL device model and framework version. The
|
|
15
|
+
launcher runs the ghcr.io/excusemi/trmnlp-test Docker image.'
|
|
16
|
+
executables:
|
|
17
|
+
- trmnlp-test
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/trmnlp-test
|
|
24
|
+
- lib/trmnlp_test/version.rb
|
|
25
|
+
homepage: https://github.com/ExcuseMi/trmnlp-test
|
|
26
|
+
licenses:
|
|
27
|
+
- MIT
|
|
28
|
+
metadata:
|
|
29
|
+
source_code_uri: https://github.com/ExcuseMi/trmnlp-test
|
|
30
|
+
rubygems_mfa_required: 'true'
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.0'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.6.9
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Test framework for TRMNL plugins (trmnlp projects, serverless, webhooks,
|
|
48
|
+
every device)
|
|
49
|
+
test_files: []
|