Skip to content

Commit a2ba870

Browse files
committed
first commit
0 parents  commit a2ba870

File tree

12 files changed

+1715
-0
lines changed

12 files changed

+1715
-0
lines changed

.github/workflows/publish.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Build avrdude with Emscripten
2+
3+
on:
4+
push:
5+
tags:
6+
- v*
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v2
15+
16+
- name: Install dependencies
17+
run: sudo apt update && sudo apt install gcc-avr avr-libc freeglut3-dev arduino-core-avr
18+
19+
- name: Setup NodeJS
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 20.x
23+
registry-url: 'https://registry.npmjs.org'
24+
25+
- name: Build SimAVR
26+
run: yarn build:simavr
27+
28+
- name: Install Dependencies
29+
run: yarn install --frozen-lockfile
30+
31+
- name: Build NPM Module
32+
run: yarn build
33+
34+
- name: Publish to NPM
35+
env:
36+
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
37+
run: npm publish --access public

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.idea
2+
build
3+
build_assets
4+
node_modules
5+
dist

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Playwright Arduino
2+
3+
Mocks the WebSerial API to test Arduino Uploaders Playwright
4+
5+
## Usage
6+
7+
Install the package with `yarn add -D @leaphy-robotics/playwright-arduino` or using NPM `npm i --save-dev @leaphy-robotics/playwright-arduino`.
8+
9+
``js
10+
import { test, expect } from '@playwright/test';
11+
import setup from '@leaphy-robotics/playwright-arduino';
12+
13+
test('test', async ({ page }) => {
14+
await setup(page);
15+
16+
// Your test code
17+
...
18+
});
19+
``
20+
21+
## Development
22+
23+
### Building simulator
24+
This step is required to be performed at least once `yarn build:simavr`
25+
26+
### Watching package
27+
You can watch for changes and automatically recompile the NPM Module using `yarn watch`
28+
29+
### Using local package
30+
Link the module using `yarn link`, now use it in your (test) project using `yarn link @leaphy-robotics/playwright-arduino`

build.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
set -e
2+
rm -rf build_assets build
3+
4+
BUILD_PATH=$(realpath build)
5+
mkdir build_assets build && cd build_assets
6+
7+
git clone https://github.com/buserror/simavr
8+
cd simavr
9+
10+
make -j8
11+
cd examples/board_simduino
12+
13+
EXECUTABLE=$(find . | grep simduino.elf)
14+
cp ATmegaBOOT_168_atmega328.ihex "${EXECUTABLE}" "${BUILD_PATH}"

package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@leaphy-robotics/playwright-arduino",
3+
"version": "1.0.0",
4+
"license": "LGPL-3.0-only",
5+
"main": "./dist/index.js",
6+
"type": "module",
7+
"types": "./dist/index.d.ts",
8+
"files": [
9+
"dist",
10+
"build"
11+
],
12+
"scripts": {
13+
"build": "tsup src",
14+
"watch": "tsup --watch src",
15+
"build:simavr": "./build.sh"
16+
},
17+
"devDependencies": {
18+
"@types/node": "^20.12.7",
19+
"@types/serialport": "^8.0.5",
20+
"@types/w3c-web-serial": "^1.0.6",
21+
"playwright": "^1.43.0",
22+
"tsup": "^8.0.2",
23+
"tsx": "^4.7.2",
24+
"typescript": "^5.4.5"
25+
},
26+
"dependencies": {
27+
"serialport": "^12.0.0"
28+
}
29+
}

src/board.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { spawn } from 'child_process'
2+
import { ChildProcess } from "node:child_process";
3+
4+
class Board {
5+
private process: ChildProcess
6+
public port = '/tmp/simavr-uart0'
7+
8+
constructor() {
9+
this.process = spawn('./simduino.elf', {
10+
cwd: `${import.meta.dirname}/../build`
11+
})
12+
}
13+
14+
public stop() {
15+
this.process.kill()
16+
}
17+
}
18+
19+
export default Board

src/index.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import {SerialPort} from "serialport";
2+
import {Page} from "playwright";
3+
import Board from "./board.ts";
4+
import {randomUUID} from "node:crypto";
5+
import {clearTimeout} from "node:timers";
6+
import * as fs from "node:fs";
7+
8+
type CallbackEvent = {
9+
resolve: (value: unknown) => void,
10+
type: string,
11+
args: any[]
12+
}
13+
14+
declare module globalThis {
15+
let requestSerial: (options: SerialPortRequestOptions) => Promise<string>,
16+
openSerial: (id: string, options: SerialOptions) => Promise<void|Error>,
17+
readPort: (id: string) => Promise<number[]|Error>,
18+
readCallback: (data: number[]) => void,
19+
writePort: (id: string, data: number[]) => Promise<void|Error>,
20+
closePort: (id: string) => Promise<void|Error>,
21+
setSignals: (id: string, signals: SerialOutputSignals) => Promise<void|Error>,
22+
getPorts: () => Promise<string[]>,
23+
onDone: Record<string, (value: any) => void>,
24+
reader: ReadableStreamDefaultReader<CallbackEvent>
25+
}
26+
27+
const ports: Record<string, SerialPort|null> = {}
28+
export default async function setup(page: Page) {
29+
const arduino = new Board()
30+
31+
const methods: Record<string, (...args: any[]) => Promise<any>> = {
32+
async requestSerial(_page: Page, _options: SerialPortRequestOptions) {
33+
const id = randomUUID()
34+
ports[id] = null
35+
36+
return String(id)
37+
},
38+
async openSerial(_page: Page, id: string, options: SerialOptions) {
39+
if (ports[id]) throw new DOMException('Port already open!')
40+
41+
return new Promise<void|Error>(resolve => {
42+
ports[id] = new SerialPort({
43+
baudRate: options.baudRate,
44+
path: arduino.port,
45+
dataBits: 8,
46+
parity: 'none',
47+
stopBits: 1,
48+
}, err => {
49+
if (err) return resolve(err)
50+
resolve()
51+
})
52+
})
53+
},
54+
async readPort(page: Page, id: string) {
55+
if (!ports[id]) throw new Error(`User read request to undefined port: ${id}`)
56+
57+
const port = ports[id] as SerialPort
58+
try {
59+
let buffer: number[]|null = null
60+
let timeout: NodeJS.Timeout|null
61+
port.on('data', async (data: Buffer) => {
62+
if (!buffer) buffer = []
63+
buffer.push(...Array.from(data.values()))
64+
65+
if (timeout) clearTimeout(timeout)
66+
timeout = setTimeout(async () => {
67+
const copy = buffer
68+
buffer = null
69+
70+
await page.evaluate((result) => {
71+
if (!result) return
72+
globalThis.readCallback(result)
73+
}, copy)
74+
}, 25)
75+
})
76+
} catch (e) {
77+
return e
78+
}
79+
},
80+
async writePort(_page: Page, id: string, data: number[]) {
81+
if (!ports[id]) throw new Error(`User write request to undefined port: ${id}`)
82+
83+
const port = ports[id] as SerialPort
84+
return new Promise<void|Error>(resolve => {
85+
port.write(Buffer.from(data), err => {
86+
if (err) return resolve(err)
87+
resolve()
88+
})
89+
})
90+
},
91+
async closePort(_page: Page, id: string) {
92+
if (!ports[id]) throw new Error(`User close request to undefined port: ${id}`)
93+
94+
const port = ports[id] as SerialPort
95+
ports[id] = null
96+
return new Promise<void|Error>(resolve => port.close(err => {
97+
if (err) return resolve(err)
98+
resolve()
99+
}))
100+
},
101+
setSignals(_page: Page, id: string, signals: SerialOutputSignals) {
102+
if (!ports[id]) throw new Error(`User setSignals request to undefined port: ${id}`)
103+
104+
const port = ports[id] as SerialPort
105+
return new Promise<void|Error>(resolve => {
106+
port.set({
107+
dtr: signals.dataTerminalReady,
108+
rts: signals.requestToSend,
109+
brk: signals.break
110+
}, () => {
111+
resolve()
112+
})
113+
})
114+
},
115+
async getPorts(_page: Page) {
116+
return Array.from(Object.keys(ports))
117+
}
118+
}
119+
120+
await Promise.all(Object.entries(methods).map(async ([type, implementation]) => {
121+
await page.exposeFunction(type, (...args: any[]) => implementation(page, ...args))
122+
}))
123+
124+
await page.route('**/avrdude-worker.js', async route => {
125+
const response = await route.fetch();
126+
const script = await response.text();
127+
await route.fulfill({ response, body: `${fs.readFileSync(`${import.meta.dirname}/page.js`)}\n\n${script}` });
128+
});
129+
130+
await page.addInitScript({
131+
path: `${import.meta.dirname}/page.js`
132+
})
133+
134+
page.on('worker', async worker => {
135+
let open = true
136+
worker.on('close', () => open = false)
137+
while (open) {
138+
try {
139+
const action = await worker.evaluate(async () => {
140+
if (!globalThis.reader) return
141+
142+
const {value, done} = await globalThis.reader.read()
143+
if (done || !value) return
144+
145+
const execution = crypto.randomUUID()
146+
globalThis.onDone[execution] = value.resolve
147+
return {
148+
execution,
149+
type: value.type,
150+
args: value.args
151+
}
152+
})
153+
154+
if (!action) continue
155+
if (!methods[action.type]) continue
156+
157+
methods[action.type](worker, ...action.args).then(async result => {
158+
await worker.evaluate(async ({ execution, result }) => {
159+
globalThis.onDone[execution](result)
160+
}, {
161+
execution: action.execution,
162+
result
163+
})
164+
})
165+
} catch { /* Once the worker has closed it will throw */ }
166+
}
167+
})
168+
169+
}

0 commit comments

Comments
 (0)