diff --git a/blinkywww/.gitignore b/blinkywww/.gitignore new file mode 100644 index 0000000..a41aa66 --- /dev/null +++ b/blinkywww/.gitignore @@ -0,0 +1,6 @@ +# Playwright +node_modules/ +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/blinkywww/README b/blinkywww/README index 1dbc8c5..187db50 100644 --- a/blinkywww/README +++ b/blinkywww/README @@ -1,5 +1,32 @@ This is the code that runs the Blinky Programmer website: http://www.wayneandlayne.com/blinky_programmer/ -Sorry that the code is probably really awkward or doing lots of things wrong. We're not really web developers, and only know enough to get by (also known as "knowing just enough to be dangerous"). +We wrote this code circa 2010? It could use some love. +# Development + +Try `npm install` to install the dependencies. If you don't have npm installed, you can try that. + +`npm install` + +You can serve the site locally with `npm run start` which starts a local web server at http://localhost:3000. + +## Testing + +We use Playwright. You can run `npm run test` but you can also use `npx playwright test` to run the tests directly. + +# TODO (before merging this in) + +* Flexbox for the layout +* Remove all floats +* Test on small screens +* Try requestAnimationFrame instead of setTimeout +* Adjust text, visual design +* Maybe pull the images for LED and use background color on round divs instead? +* Add more pixel mode tests + * multiple messages + * mixed pixel and text messages + * all the pixel options + * delete some lines, add some lines + * make sure the clicked pixels are "on" and the others are off +* Test on a real Blinky diff --git a/blinkywww/index.php b/blinkywww/index.php deleted file mode 100644 index 84774d0..0000000 --- a/blinkywww/index.php +++ /dev/null @@ -1,534 +0,0 @@ - - - - \"Move\"

- \"Delete\" - - - Type: - -   -   - - Style: - -
- - End behavior:    Delay: - -
- -
Message:
Available characters: 0-9, A-Z, !, ?, &, \',\', and \'.\'.
- -
- Click to set LEDs. Click X to remove column. -
-
-
- - -"; - -$token = strtok($new_div_raw, "\n"); -$new_div = ""; -while ($token != false) -{ - $new_div .= $token; - $token = strtok("\n"); -} - -$led_line = ""; -for ($i = 0; $i < 8; $i++) - $led_line .= "
"; -$led_line .= ""; -?> - - - -Blinky POV and Blinky GRID Programming - - - - - - -

Blinky POV and Blinky GRID Programming

-

This webpage will let you design and upload new messages to your Blinky POV or Blinky GRID kit. First, you should create one or more messages in the first section below. Once your messages are prepared, section 2 below will let you transmit the new message(s) to your Blinky kit.

- -

1. Create Messages

-

You can store multiple messages on your Blinky kit. Messages can be either text-based or pixel-based. Font-based messages take up much less memory than pixel-based messages. Marquee/Animation only matters on Blinky GRID. For delay, 0 is the fastest value and 15 is the slowest value. Drag the arrow icons to reorder your messages. Use the garbage can to remove a message.

-
-
- - - -

2. Programming

-

Enter programming mode: Hold down the button while turning on the power switch. Release the button. Hold the Blinky up to the screen. Be sure to align the sensors correctly: The sensor labeled C points to clock, and D points to data. Press and release the button when you're ready to begin transmission. Press the Go button to start.

-


Check out this awesome animated guide to programming!

-

Please note! On 2011/09/10 we switched the locations of the clock and data squares below to make it easier to hold the Blinky device while programming, based on customer feed back we have received over the past few months. This requires no change in the Blinky firmware, only a change in how you align the sensors on the screen. Please let us know if this change is a dealbreaker.

-
-
-
- -
Progress:
-
-
- - -
- Delay (ms):
-
-
-
Data
-
Clock
-
-
-

3. Troubleshooting

-

For the delay value, smaller values equal quicker data transmissions, but too small/fast might cause errors! The smallest reliable value depends on your monitor, browser, and computer graphics hardware. Start with a value of 40, and make sure it can reliably transfer your messages. You can then gradually reduce the delay time until you start having invalid transmissions, indicated by alternating flashing of the third and fourth LEDs on your Blinky board. We've have good luck with values of 25-40 on various systems we've tested.

-

During normal transmission, the second LED will flash once for each byte of data received. When the transmission is finished, the blinky should immediately start displaying your messages. The blinky data is transmitted as a series of one or more records. After each record is transmitted, the blinky will double-check the data to make sure it was properly transmitted (using something called a checksum). If the checksum is invalid, meaning that there was a data transmission problem, the third and fourth LEDs on the blinky will flash rapidly in an alternating pattern. If this happens, press the Stop button, reset your blinky into the programming (bootloader) mode, and try again. If you continue to experience trouble, try increasing the value in the delay box and set your monitor's brightness to 100%. If nothing works, try the forums.

- -
-
-Debug information:
-message_data:
-
-message_data hex:
-
-
-xmit_data:
-
-xmit_data hex:
-
-
- -
-xmit_raw:
- -
- - diff --git a/blinkywww/package-lock.json b/blinkywww/package-lock.json new file mode 100644 index 0000000..261501e --- /dev/null +++ b/blinkywww/package-lock.json @@ -0,0 +1,1067 @@ +{ + "name": "blinky", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "blinky", + "version": "1.0.0", + "dependencies": { + }, + "devDependencies": { + "playwright": "^1.53.1", + "serve": "^14.2.4", + "@playwright/test": "^1.53.1", + "@types/node": "^24.0.4" + } + }, + "node_modules/@playwright/test": { + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", + "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.53.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", + "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.53.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", + "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/serve": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", + "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.7.4", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.6", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + } + } +} diff --git a/blinkywww/package.json b/blinkywww/package.json new file mode 100644 index 0000000..b881665 --- /dev/null +++ b/blinkywww/package.json @@ -0,0 +1,21 @@ +{ + "name": "blinky", + "version": "1.0.0", + "description": "Blinky POV/GRID programming interface", + "author": "Adam Wolf, Matthew Beckler", + "main": "index.js", + "directories": { + "test": "playwright test" + }, + "scripts": { + "start": "serve src/" + }, + "dependencies": { + "playwright": "^1.53.1", + "serve": "^14.2.4" + }, + "devDependencies": { + "@playwright/test": "^1.53.1", + "@types/node": "^24.0.4" + } +} diff --git a/blinkywww/playwright.config.js b/blinkywww/playwright.config.js new file mode 100644 index 0000000..9d6992c --- /dev/null +++ b/blinkywww/playwright.config.js @@ -0,0 +1,80 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + + /* Test against branded browsers. */ + { + name: 'Microsoft Edge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + } +}); diff --git a/blinkywww/progress_bar_30px_tall.png b/blinkywww/progress_bar_30px_tall.png deleted file mode 100644 index 9489bab..0000000 Binary files a/blinkywww/progress_bar_30px_tall.png and /dev/null differ diff --git a/blinkywww/SOTC-DnDLists.js b/blinkywww/src/SOTC-DnDLists.js similarity index 100% rename from blinkywww/SOTC-DnDLists.js rename to blinkywww/src/SOTC-DnDLists.js diff --git a/blinkywww/src/assets/blinky_programming.gif b/blinkywww/src/assets/blinky_programming.gif new file mode 100644 index 0000000..9837219 Binary files /dev/null and b/blinkywww/src/assets/blinky_programming.gif differ diff --git a/blinkywww/src/assets/blinky_programming_crop.png b/blinkywww/src/assets/blinky_programming_crop.png new file mode 100644 index 0000000..a1e2193 Binary files /dev/null and b/blinkywww/src/assets/blinky_programming_crop.png differ diff --git a/blinkywww/delete.png b/blinkywww/src/delete.png similarity index 100% rename from blinkywww/delete.png rename to blinkywww/src/delete.png diff --git a/blinkywww/drag.css b/blinkywww/src/drag.css similarity index 100% rename from blinkywww/drag.css rename to blinkywww/src/drag.css diff --git a/blinkywww/get_chrome.png b/blinkywww/src/get_chrome.png similarity index 100% rename from blinkywww/get_chrome.png rename to blinkywww/src/get_chrome.png diff --git a/blinkywww/get_firefox.png b/blinkywww/src/get_firefox.png similarity index 100% rename from blinkywww/get_firefox.png rename to blinkywww/src/get_firefox.png diff --git a/blinkywww/src/index.html b/blinkywww/src/index.html new file mode 100644 index 0000000..8122bbd --- /dev/null +++ b/blinkywww/src/index.html @@ -0,0 +1,549 @@ + + + + Blinky POV and Blinky GRID Programming + + + + + + + + + +
+

Blinky POV and Blinky GRID Programming

+

This webpage will let you design and upload new messages to your Blinky POV or Blinky GRID kit. First, you should create one or + more messages in the first section below. Once your messages are prepared, section 2 below will let you transmit + the new message(s) to your Blinky kit.

+ +

1. Create Messages

+

You can store multiple messages on your Blinky kit. Messages can be either text-based or pixel-based. Font-based + messages take up much less memory than pixel-based messages. Marquee/Animation only matters on Blinky GRID. For + delay, 0 is the fastest value and 15 is the slowest value. Drag the arrow icons to reorder your messages. Use + the garbage can to remove a message.

+
+
+ + + +

2. Programming

+

Enter programming mode: Hold down the button while turning on the power switch. Release the button. Hold the + Blinky up to the screen. Be sure to align the sensors correctly: The sensor labeled C points to clock, and D + points to data. Press and release the button when you're ready to begin transmission. Press the Go button to + start.

+

+ + +
+ Check out this awesome animated guide to programming! +
+

+
+
+
+
+ +
+
+ + +
+
+ +
+
Progress: Ready
+ + +
+
+ +
+
Clock
+
Data
+
+
+

3. Troubleshooting

+

For the delay value, smaller values equal quicker data transmissions, but too small/fast might cause errors! The + smallest reliable value depends on your monitor, browser, and computer graphics hardware. Start with a value of + 40, and make sure it can reliably transfer your messages. You can then gradually reduce the delay time until you + start having invalid transmissions, indicated by alternating flashing of the third and fourth LEDs on your + Blinky board. We've have good luck with values of 25-40 on various systems we've tested.

+

During normal transmission, the second LED will flash once for each byte of data received. When the transmission + is finished, the blinky should immediately start displaying your messages. The blinky data is transmitted as a + series of one or more records. After each record is transmitted, the blinky will double-check the data + to make sure it was properly transmitted (using something called a checksum). If the checksum is invalid, meaning that + there was a data transmission problem, the third and fourth LEDs on the blinky will flash rapidly in an + alternating pattern. If this happens, press the Stop button, reset your blinky into the programming (bootloader) + mode, and try again. If you continue to experience trouble, try increasing the value in the delay box and set + your monitor's brightness to 100%. If nothing works, try the forums.

+ +

Changelog

+
    +
  1. + 2025/06/??: We updated much of this page. If you preferred the old version, please let us know why! +
  2. + +
  3. + 2011/09/10: We switched the locations of the clock and data squares below to make + it easier to hold the Blinky device while programming, based on customer feedback we have received over the + past few months. This requires no change in the Blinky firmware, only a change in how you align the sensors on + the screen. Please let us know if this change is a problem. +
  4. +
+ +
+
+ Debug information:
+ message_data:
+
+ message_data hex:
+
+
+ xmit_data:
+
+ xmit_data hex:
+
+
+ +
+ xmit_raw:
+ +
+
+ + diff --git a/blinkywww/led_dark.png b/blinkywww/src/led_dark.png similarity index 100% rename from blinkywww/led_dark.png rename to blinkywww/src/led_dark.png diff --git a/blinkywww/led_lit.png b/blinkywww/src/led_lit.png similarity index 100% rename from blinkywww/led_lit.png rename to blinkywww/src/led_lit.png diff --git a/blinkywww/led_x.png b/blinkywww/src/led_x.png similarity index 100% rename from blinkywww/led_x.png rename to blinkywww/src/led_x.png diff --git a/blinkywww/move.gif b/blinkywww/src/move.gif similarity index 100% rename from blinkywww/move.gif rename to blinkywww/src/move.gif diff --git a/blinkywww/msg_anim.gif b/blinkywww/src/msg_anim.gif similarity index 100% rename from blinkywww/msg_anim.gif rename to blinkywww/src/msg_anim.gif diff --git a/blinkywww/msg_marq.gif b/blinkywww/src/msg_marq.gif similarity index 100% rename from blinkywww/msg_marq.gif rename to blinkywww/src/msg_marq.gif diff --git a/blinkywww/src/style.css b/blinkywww/src/style.css new file mode 100644 index 0000000..c7dba35 --- /dev/null +++ b/blinkywww/src/style.css @@ -0,0 +1,177 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; + font-family: system-ui, sans-serif; +} + +img { + max-width: 100%; + height: auto; + display: block; +} + +a { + color: #90CAF9; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +body { + line-height: 1.5; + min-height: 100vh; +} + +main { + max-width: 65ch; + margin: auto; /* center */ + padding: 0 2em; +} + +p, li { + text-wrap: pretty; + margin-bottom: 1em; +} + +h1, h2, h3, h4, h5, h6 { + text-wrap: balance; + margin-bottom: 0.5em; +} + +/* Inherit fonts for inputs and buttons */ +input, button, +textarea, select { + font-family: inherit; + font-size: inherit; +} + +[id$="_pixel_leds"] { + display: grid; + grid-auto-flow: column; + grid-template-rows: repeat(9, auto); + gap: 0; + width: fit-content; +} + +[id$="_pixel_leds"] > div { + float: none !important; + width: auto !important; + display: contents; +} + +[id$="_pixel_leds"] img { + width: 16px; + height: 16px; + display: block; +} + +#list { + position: relative; + border: 1px solid white; + width: 575px; +} + +/* Native progress element styling */ +.progress-wrapper { + margin-bottom: 10px; +} + +progress { + width: 100%; + height: 30px; +} + +/* WebKit browsers (Chrome, Safari) */ +progress::-webkit-progress-bar { + background-color: #f0f0f0; + border: 2px solid white; + border-radius: 2px; +} + +progress::-webkit-progress-value { + background-color: #4CAF50; + border-radius: 0; + transition: width 0.3s ease; + appearance: none; + -webkit-appearance: none; +} + +/* Firefox */ +progress::-moz-progress-bar { + background-color: #4CAF50; + border-radius: 0; + transition: width 0.3s ease; +} + +progress { + color: #4CAF50; +} + +.progress-status { + padding: 5px 0; + font-weight: bold; +} + +ol { + list-style: none; + padding-inline-start: 0; +} + +.transmission-panel { + display: flex; + flex-wrap: wrap; + gap: 0.5em; + justify-content: center; +} + +.data-transmission-panel { + display: flex; +} + +.transmission-panel button { + height: 50px; + width: 60px; + font-size: 100%; + font-weight: bold; + margin: 0.5em; +} + +.transmission-square { + width: 150px; + height: 150px; + background-color: black; + border: 2px solid gray; + color: gray; + display: flex; + justify-content: center; + padding: 0.5em; +} + +#divclock { + border-right: 1px dashed gray; +} + +#divdata { + border-left: 1px dashed gray; +} + +.programming-controls { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: center; +} \ No newline at end of file diff --git a/blinkywww/white.png b/blinkywww/src/white.png similarity index 100% rename from blinkywww/white.png rename to blinkywww/src/white.png diff --git a/blinkywww/tests/test.spec.js b/blinkywww/tests/test.spec.js new file mode 100644 index 0000000..29899df --- /dev/null +++ b/blinkywww/tests/test.spec.js @@ -0,0 +1,340 @@ +const { test, expect } = require('@playwright/test'); + +test.describe('eeprom data', () => { + test('verify bitstream for happy path simple text message', async ({ page }) => { + await page.goto('/'); + + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('TEST'); + + await page.getByRole('button', { name: 'Go' }).click(); + const messageData = await page.locator('#message_data').inputValue(); + + expect(messageData).toEqual('1,30,4,29,14,28,29'); + // 1 message, 30 is the config byte for the message, 4 data bytes in message, T=29, E=14, S=28, T=29 + }); + + test('verify bitstream for simple pixel message', async ({ page }) => { + await page.goto('http://localhost:3000/'); + + await page.getByRole('radio', { name: 'Pixel' }).check(); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + + await page.locator('div > img').first().click(); + await page.locator('div:nth-child(2) > img:nth-child(2)').click(); + await page.locator('div:nth-child(3) > img:nth-child(3)').click(); + await page.locator('div:nth-child(4) > img:nth-child(4)').click(); + await page.locator('div:nth-child(5) > img:nth-child(5)').click(); + await page.locator('div:nth-child(6) > img:nth-child(6)').click(); + await page.locator('div:nth-child(7) > img:nth-child(7)').click(); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,158,7,128,64,32,16,8,4,2'); + }); + + test('verify bitstream for multiple text messages', async ({ page }) => { + await page.goto('/'); + + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('Hello'); + + await page.locator('input[value="Add New Message"]').click(); + await page.locator('#msg_1_anim_marq').check(); + await page.locator('#msg_1_end_type').selectOption('Advance to next message'); + await page.locator('#msg_1_delay').selectOption('7'); + await page.locator('#msg_1_text_message').fill('World'); + + await page.getByRole('button', { name: 'Go' }).click(); + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('2,30,5,17,14,21,21,24,30,5,32,24,27,21,13'); + }); + + test('verify bitstream for animation-style text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_anim').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('animation'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,94,9,10,23,18,22,10,29,18,24,23'); + }); + + test('verify bitstream for marquee-style text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('marquee'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,30,7,22,10,27,26,30,14,14'); + }); + + test('verify bitstream for text message that ends by stopping', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Stop'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('stop'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,28,4,28,29,24,25'); + }); + + test('verify bitstream for text message that ends by repeating', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Repeat this message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('repeat'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,29,6,27,14,25,14,10,29'); + }); + + test('verify bitstream for text message that ends by advancing', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('advance'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,30,7,10,13,31,10,23,12,14'); + }); + + test('verify bitstream for easter egg text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Easter egg...'); + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('egg'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,31,3,14,16,16'); + }); + + test('verify bitstream for low delay text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); // advance + await page.locator('#msg_0_delay').selectOption('0'); + await page.locator('#msg_0_text_message').fill('fast'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,2,4,15,10,28,29'); + }); + + test('verify bitstream for middle delay text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); // advance + await page.locator('#msg_0_delay').selectOption('5'); + await page.locator('#msg_0_text_message').fill('medium'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,22,6,22,14,13,18,30,22'); + }); + + test('verify bitstream for slow delay text message', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('Advance to next message'); // advance + await page.locator('#msg_0_delay').selectOption('15'); + await page.locator('#msg_0_text_message').fill('sloooow'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,62,7,28,21,24,24,24,24,32'); + }); + + test('verify bitstream for all the advertised text characters', async ({ page }) => { + await page.goto('/'); + await page.locator('#msg_0_anim_marq').check(); + await page.locator('#msg_0_end_type').selectOption('2'); // advance + await page.locator('#msg_0_delay').selectOption('7'); + await page.locator('#msg_0_text_message').fill('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!?&,.'); + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('1,30,67,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,37,39,38,40,36'); + }); +}); + + test('verify initial square state is black for data and clock', async ({ page }) => { + await page.goto('/'); + + const clockBg = await page.evaluate(() => { + return window.getComputedStyle(document.getElementById('divclock')).backgroundColor; + }); + + const dataBg = await page.evaluate(() => { + return window.getComputedStyle(document.getElementById('divdata')).backgroundColor; + }); + + // Both should start as black (rgb(0, 0, 0)) + expect(clockBg).toBe('rgb(0, 0, 0)'); + expect(dataBg).toBe('rgb(0, 0, 0)'); + }); + + + test('verify stop button resets to black', async ({ page }) => { + await page.goto('/'); + + await page.locator('#msg_0_text_message').fill('TEST'); + await page.getByRole('button', { name: 'Go' }).click(); + await page.waitForTimeout(100); + await page.getByRole('button', { name: 'Stop' }).click(); + await page.waitForTimeout(50); + + const clockBg = await page.evaluate(() => { + return window.getComputedStyle(document.getElementById('divclock')).backgroundColor; + }); + + const dataBg = await page.evaluate(() => { + return window.getComputedStyle(document.getElementById('divdata')).backgroundColor; + }); + + expect(clockBg).toBe('rgb(0, 0, 0)'); + expect(dataBg).toBe('rgb(0, 0, 0)'); + }); + + test('verify pixel animation data padding', async ({ page }) => { + await page.goto('/'); + + // Switch to pixel mode with animation + await page.getByRole('radio', { name: 'Pixel' }).check(); + await page.locator('#msg_0_anim_anim').check(); + + // Set some pixels in first 3 columns + await page.locator('#msg_0_pixel_leds > div > img').first().click(); + await page.locator('#msg_0_pixel_leds > div:nth-child(2) > img:nth-child(3)').click(); + await page.locator('#msg_0_pixel_leds > div:nth-child(3) > img:nth-child(5)').click(); + + await page.getByRole('button', { name: 'Go' }).click(); + + const messageData = await page.locator('#message_data').inputValue(); + // Should be padded to 7 columns for animation mode + expect(messageData).toBe('1,222,7,128,32,8,0,0,0,0'); + // Config byte 222 = 0xDE = 11011110 (pixel=1, anim=1, delay=7, advance=2) + }); + + +test('verify delete message with drag objects', async ({ page }) => { + await page.goto('/'); + + // Add multiple messages + await page.locator('input[value="Add New Message"]').click(); + await page.locator('input[value="Add New Message"]').click(); + + // Try to delete the middle message + await page.locator('#msg_1 .delete').click(); + + // Handle the confirmation dialog + page.on('dialog', dialog => dialog.accept()); + + // Click delete again with dialog handler active + await page.locator('#msg_1 .delete').click(); + + // Verify message is removed + await expect(page.locator('#msg_1')).not.toBeVisible(); + + // Verify other messages still exist + await expect(page.locator('#msg_0')).toBeVisible(); + await expect(page.locator('#msg_2')).toBeVisible(); +}); + +test('verify message ordering after deletion', async ({ page }) => { + await page.goto('/'); + + // Add 3 messages + await page.locator('input[value="Add New Message"]').click(); + await page.locator('input[value="Add New Message"]').click(); + + await page.locator('#msg_0_text_message').fill('FIRST'); + await page.locator('#msg_1_text_message').fill('SECOND'); + await page.locator('#msg_2_text_message').fill('THIRD'); + + // Delete middle message + page.on('dialog', dialog => dialog.accept()); + await page.locator('#msg_1 .delete').click(); + + // make sure they're in the right order on the screen + + // Verifies messages remain visible in correct positions + await expect(page.locator('#msg_0')).toBeVisible(); + await expect(page.locator('#msg_2')).toBeVisible(); + + // Checks vertical positioning + const msg0Rect = await page.locator('#msg_0').boundingBox(); + const msg2Rect = await page.locator('#msg_2').boundingBox(); + expect(msg0Rect.y).toBeLessThan(msg2Rect.y); + + //make sure msg_1 doesn't exist anymore + await expect(page.locator('#msg_1')).not.toBeVisible(); + + // Transmit and verify order + await page.getByRole('button', { name: 'Go' }).click(); + + // check message data + const messageData = await page.locator('#message_data').inputValue(); + expect(messageData).toEqual('2,30,5,15,18,27,28,29,30,5,29,17,18,27,13'); +}); + + + +test.describe('transmission data validation', () => { + test('verify transmission data', async ({ page }) => { + await page.goto('/'); + + await page.locator('#msg_0_text_message').fill('A'); + await page.getByRole('button', { name: 'Go' }).click(); + + const xmitDataHex = await page.locator('#xmit_data_hex').inputValue(); + const hexValues = xmitDataHex.split(','); + + expect(xmitDataHex).toEqual('04,00,00,06,01,1E,01,0A,CC,00,00,00,01,FF'); + }); + + test('verify 16-byte record splitting', async ({ page }) => { + await page.goto('/'); + + // Create a message longer than 16 bytes + await page.locator('#msg_0_text_message').fill('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); + await page.getByRole('button', { name: 'Go' }).click(); + + const xmitDataHex = await page.locator('#xmit_data_hex').inputValue(); + + expect(xmitDataHex).toEqual('10,00,00,06,01,1E,1A,0A,0B,0C,0D,0E,0F,10,11,12,13,14,15,16,E1,0D,00,10,06,17,18,19,1A,1B,1C,1D,1E,1F,20,21,22,23,64,00,00,00,01,FF'); + }); + + test('verify end-of-transmission record', async ({ page }) => { + await page.goto('/'); + + await page.locator('#msg_0_text_message').fill('TEST'); + await page.getByRole('button', { name: 'Go' }).click(); + + const xmitData = await page.locator('#xmit_data').inputValue(); + const bytes = xmitData.split(',').map(Number); + + // Last 5 bytes should be the reset record: 0,0,0,1,255 + const lastFive = bytes.slice(-5); + expect(lastFive).toEqual([0, 0, 0, 1, 255]); + }); +});