mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Add playwright for e2e testing
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
name: check-e2e
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
check-e2e:
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [webkit]
|
||||
timeout-minutes: 30
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build test-http-api
|
||||
run: npm run build:http
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
id: playwright-dep-cache
|
||||
with:
|
||||
path: ~/Library/Caches/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: steps.playwright-dep-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install webkit
|
||||
|
||||
- name: Install Radicle binaries
|
||||
run: |
|
||||
mkdir -p tests/artifacts;
|
||||
./scripts/install-binaries;
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:e2e -- --project ${{ matrix.browser }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-artifacts-${{ runner.os }}
|
||||
retention-days: 30
|
||||
if-no-files-found: "ignore"
|
||||
path: |
|
||||
tests/artifacts/**/*
|
||||
@@ -23,8 +23,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: crates/radicle-tauri -> target
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: libgtk-3-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
@@ -22,3 +22,7 @@ public/twemoji/*.svg
|
||||
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
|
||||
# Integration Tests
|
||||
tests/tmp/**/*
|
||||
tests/artifacts/**/*
|
||||
|
||||
Generated
+515
-9
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.0.3",
|
||||
@@ -24,14 +25,18 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
"baconjs": "^3.0.19",
|
||||
"bs58": "^6.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^5.3.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"execa": "^9.5.0",
|
||||
"get-port": "^7.1.0",
|
||||
"hast-util-to-dom": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.3",
|
||||
@@ -48,7 +53,8 @@
|
||||
"twemoji": "^14.0.2",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
"vite": "^5.4.9"
|
||||
"vite": "^5.4.9",
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.9.0"
|
||||
@@ -547,6 +553,21 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@hapi/topo": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
||||
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz",
|
||||
@@ -678,6 +699,21 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
|
||||
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.48.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radicle/gray-matter": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radicle/gray-matter/-/gray-matter-4.1.0.tgz",
|
||||
@@ -901,6 +937,45 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/formula": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sideway/pinpoint": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0.tgz",
|
||||
@@ -1235,6 +1310,15 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/wait-on": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz",
|
||||
"integrity": "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz",
|
||||
@@ -1545,6 +1629,23 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -1657,16 +1758,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
@@ -1705,6 +1802,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
@@ -1779,6 +1888,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
@@ -2056,6 +2174,22 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"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/esm-env": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
|
||||
@@ -2131,6 +2265,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.0.tgz",
|
||||
"integrity": "sha512-t7vvYt+oKnMbF3O+S5+HkylsPrsUatwJSe4Cv+4017R0MCySjECxnVJ2eyDXVD/Xpj5H29YzyYn6eEpugG7GJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
@@ -2212,6 +2372,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -2271,6 +2446,40 @@
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
@@ -2308,6 +2517,34 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-port": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz",
|
||||
"integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -2368,6 +2605,15 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
|
||||
"integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -2471,6 +2717,18 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
@@ -2480,12 +2738,49 @@
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.13.3",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
|
||||
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.3.0",
|
||||
"@hapi/topo": "^5.1.0",
|
||||
"@sideway/address": "^4.1.5",
|
||||
"@sideway/formula": "^3.0.1",
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@@ -2741,6 +3036,27 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dev": true,
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -2753,6 +3069,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -2792,6 +3117,34 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -2851,6 +3204,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -2889,6 +3254,50 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
|
||||
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.48.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
|
||||
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
@@ -3035,6 +3444,21 @@
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz",
|
||||
"integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
|
||||
@@ -3045,6 +3469,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -3164,6 +3594,15 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
@@ -3222,6 +3661,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -3240,6 +3691,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -3497,6 +3960,18 @@
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
@@ -3606,6 +4081,25 @@
|
||||
"integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz",
|
||||
"integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"joi": "^17.13.3",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.8",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"wait-on": "bin/wait-on"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-namespaces": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
||||
@@ -3661,6 +4155,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
||||
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
|
||||
+9
-1
@@ -8,11 +8,13 @@
|
||||
"start": "vite",
|
||||
"start:http": "cargo run --manifest-path ./crates/test-http-api/Cargo.toml",
|
||||
"build": "vite build && scripts/copy-katex-assets && scripts/install-twemoji-assets",
|
||||
"build:http": "cargo build --manifest-path ./crates/test-http-api/Cargo.toml",
|
||||
"postinstall": "scripts/copy-katex-assets && scripts/install-twemoji-assets",
|
||||
"preview": "vite preview",
|
||||
"check": "scripts/check-js && scripts/check-rs",
|
||||
"check-js": "scripts/check-js",
|
||||
"check-rs": "scripts/check-rs",
|
||||
"test:e2e": "TZ='UTC' playwright test",
|
||||
"format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
|
||||
"generate-types": "cargo test --manifest-path ./crates/radicle-types/Cargo.toml && npx prettier ./crates/radicle-types/bindings --write",
|
||||
"tauri": "npx tauri"
|
||||
@@ -29,6 +31,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.0.3",
|
||||
@@ -36,14 +39,18 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
"baconjs": "^3.0.19",
|
||||
"bs58": "^6.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^5.3.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"execa": "^9.5.0",
|
||||
"get-port": "^7.1.0",
|
||||
"hast-util-to-dom": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.3",
|
||||
@@ -60,6 +67,7 @@
|
||||
"twemoji": "^14.0.2",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
"vite": "^5.4.9"
|
||||
"vite": "^5.4.9",
|
||||
"wait-on": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
outputDir: "./tests/artifacts",
|
||||
testDir: "./tests/e2e",
|
||||
globalSetup: "./tests/support/globalSetup.ts",
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 8000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: "list",
|
||||
use: {
|
||||
colorScheme: "dark",
|
||||
actionTimeout: 5000,
|
||||
baseURL: "http://localhost:3001",
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices["Desktop Safari"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
webServer: [
|
||||
{
|
||||
command: "npm run start -- --strictPort --port 3001",
|
||||
port: 3001,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
|
||||
RELEASE=$(cat "$REPO_ROOT/tests/support/heartwood-release")
|
||||
BINARY_PATH=$REPO_ROOT/tests/tmp/bin
|
||||
OS=$(uname)
|
||||
|
||||
install() {
|
||||
if test -d "$BINARY_PATH/$1/$2"; then
|
||||
echo ✅ "Folder $BINARY_PATH/$1/$2 exists already skipping download."
|
||||
else
|
||||
mkdir -p "$BINARY_PATH/$1/$2"
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
ARCH="aarch64-apple-darwin"
|
||||
;;
|
||||
Linux)
|
||||
ARCH="x86_64-unknown-linux-musl"
|
||||
;;
|
||||
*)
|
||||
echo "There are no precompiled binaries for your OS: $OS, compile $1 manually and make sure it's in PATH." && exit 1
|
||||
;;
|
||||
esac
|
||||
case "$1" in
|
||||
heartwood)
|
||||
FETCH_URL="https://files.radicle.xyz/releases/$2/radicle-$ARCH.tar.xz"
|
||||
FILENAME="radicle-$2-$ARCH"
|
||||
;;
|
||||
*)
|
||||
echo "No precompiled binary found with the name $1." && exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo Downloading "$1" v"$2" from "$FETCH_URL into /tests/tmp/bin/$1/$2"
|
||||
curl --fail -s "$FETCH_URL" | tar -xJ --strip-components=2 -C "$BINARY_PATH/$1/$2" "$FILENAME/bin/" || (echo "Download failed" && exit 1)
|
||||
fi
|
||||
}
|
||||
|
||||
show_usage() {
|
||||
echo
|
||||
echo "Installs binaries required for running e2e test suite."
|
||||
echo
|
||||
echo "USAGE:"
|
||||
echo " install-binaries [-h]"
|
||||
echo
|
||||
echo "OPTIONS:"
|
||||
echo " -h --help Print this Help."
|
||||
echo
|
||||
}
|
||||
|
||||
while [ $# -ne 0 ]; do
|
||||
case "$1" in
|
||||
--help | -h)
|
||||
show_usage
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
install "heartwood" "$RELEASE"
|
||||
|
||||
echo
|
||||
+1
-1
@@ -8,7 +8,7 @@ export async function invoke<T = null>(
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
return tauri.invoke(cmd, args, options);
|
||||
} else {
|
||||
return fetch(`http://127.0.0.1:8080/${cmd}`, {
|
||||
return fetch(`http://127.0.0.1:8081/${cmd}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(args),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function sleep(timeMs: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, timeMs);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("navigate to repo issues", async ({ page }) => {
|
||||
await page.goto("/repos");
|
||||
await page.getByRole("button", { name: "cobs" }).click();
|
||||
await page
|
||||
.getByRole("button", { name: "This title has **markdown**" })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByText("This title has **markdown**").nth(1),
|
||||
).toBeVisible();
|
||||
});
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
import type { RadiclePeer } from "@tests/support/peerManager.js";
|
||||
import type { Options } from "execa";
|
||||
|
||||
export async function create(
|
||||
peer: RadiclePeer,
|
||||
title: string,
|
||||
description: string,
|
||||
labels: string[],
|
||||
options: Options,
|
||||
): Promise<string> {
|
||||
const issueOptions: string[] = [
|
||||
"issue",
|
||||
"open",
|
||||
"--title",
|
||||
title,
|
||||
"--description",
|
||||
description,
|
||||
...labels.map(label => ["--label", label]).flat(),
|
||||
];
|
||||
const { stdout } = await peer.rad(issueOptions, options);
|
||||
const match = stdout.match(/Issue {3}([a-zA-Z0-9]*)/);
|
||||
if (!match) {
|
||||
throw new Error("Not able to parse issue id");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { RadiclePeer } from "@tests/support/peerManager.js";
|
||||
import type { Options } from "execa";
|
||||
|
||||
export async function create(
|
||||
peer: RadiclePeer,
|
||||
commitLines: string[],
|
||||
branch: string,
|
||||
changeFn: () => Promise<void>,
|
||||
messages: string[],
|
||||
options: Options,
|
||||
): Promise<string> {
|
||||
if (branch) {
|
||||
await peer.git(["reset", "--hard"], options);
|
||||
await peer.git(["switch", "main"], options);
|
||||
await peer.git(["switch", "-c", branch], options);
|
||||
}
|
||||
await changeFn();
|
||||
await peer.git(["add", "."], options);
|
||||
await peer.git(
|
||||
["commit"].concat(...commitLines.map(line => ["-m", line])),
|
||||
options,
|
||||
);
|
||||
const cmd = [
|
||||
"push",
|
||||
...messages.map(msg => ["-o", `patch.message=${msg}`]).flat(),
|
||||
"rad",
|
||||
"HEAD:refs/patches",
|
||||
];
|
||||
const { stderr } = await peer.git(cmd, options);
|
||||
const match = stderr.match(/✓ Patch ([a-zA-Z0-9]*) opened/);
|
||||
if (!match) {
|
||||
throw new Error("Not able to parse patch id");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export async function merge(
|
||||
peer: RadiclePeer,
|
||||
targetBranch: string,
|
||||
featureBranch: string,
|
||||
options: Options,
|
||||
): Promise<void> {
|
||||
await peer.git(["switch", targetBranch], options);
|
||||
await peer.git(["merge", featureBranch], options);
|
||||
await peer.git(["push", "rad", targetBranch], options);
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import type { PeerManager, RadiclePeer } from "./peerManager.js";
|
||||
import type * as Stream from "node:stream";
|
||||
|
||||
import * as Fs from "node:fs/promises";
|
||||
import * as Path from "node:path";
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { execa } from "execa";
|
||||
|
||||
import * as issue from "@tests/support/cobs/issue.js";
|
||||
import * as logLabel from "@tests/support/logPrefix.js";
|
||||
import * as patch from "@tests/support/cobs/patch.js";
|
||||
import { createOptions, supportDir, tmpDir } from "@tests/support/support.js";
|
||||
import { createPeerManager } from "@tests/support/peerManager.js";
|
||||
import { createRepo } from "@tests/support/repo.js";
|
||||
import { formatOid } from "@app/lib/utils.js";
|
||||
|
||||
export { expect };
|
||||
|
||||
const fixturesDir = Path.resolve(supportDir, "..", "./fixtures");
|
||||
|
||||
export const test = base.extend<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
forAllTests: void;
|
||||
stateDir: string;
|
||||
peerManager: PeerManager;
|
||||
peer: RadiclePeer;
|
||||
outputLog: Stream.Writable;
|
||||
}>({
|
||||
forAllTests: [
|
||||
async ({ outputLog, page }, use) => {
|
||||
const browserLabel = logLabel.logPrefix("browser");
|
||||
page.on("console", msg => {
|
||||
// Ignore common console logs that we don't care about.
|
||||
if (
|
||||
msg.text().startsWith("[vite] connected.") ||
|
||||
msg.text().startsWith("[vite] connecting...") ||
|
||||
msg.text().startsWith("Not able to parse url") ||
|
||||
msg
|
||||
.text()
|
||||
.includes("Please make sure it wasn't preloaded for nothing.")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
log(msg.text(), browserLabel, outputLog);
|
||||
});
|
||||
|
||||
if (!process.env.CONTINUE_ON_ERRORS) {
|
||||
page.on("pageerror", msg => {
|
||||
expect(
|
||||
false,
|
||||
`Test failed because there was a console error in the app: ${msg}`,
|
||||
).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
const playwrightLabel = logLabel.logPrefix("playwright");
|
||||
|
||||
function isLocalhost(url: URL) {
|
||||
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
||||
}
|
||||
|
||||
await page.route(
|
||||
url => !isLocalhost(url),
|
||||
route => {
|
||||
log(
|
||||
`Aborted remote request: ${route.request().url()}`,
|
||||
playwrightLabel,
|
||||
outputLog,
|
||||
);
|
||||
return route.abort();
|
||||
},
|
||||
);
|
||||
|
||||
await page.route(
|
||||
url =>
|
||||
url.href.startsWith("https://www.gravatar.com/avatar/") ||
|
||||
(url.href.endsWith(".png") && !isLocalhost(url)),
|
||||
route => {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
path: "./public/radicle.svg",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await use();
|
||||
},
|
||||
{ scope: "test", auto: true },
|
||||
],
|
||||
|
||||
outputLog: async ({ stateDir }, use) => {
|
||||
const logFile = await Fs.open(Path.join(stateDir, "test.log"), "a");
|
||||
await use(logFile.createWriteStream());
|
||||
await logFile.close();
|
||||
},
|
||||
|
||||
peerManager: async ({ stateDir, outputLog }, use) => {
|
||||
const peerManager = await createPeerManager({
|
||||
dataDir: Path.resolve(Path.join(stateDir, "peers")),
|
||||
outputLog,
|
||||
});
|
||||
await use(peerManager);
|
||||
await peerManager.shutdown();
|
||||
},
|
||||
|
||||
peer: async ({ peerManager }, use) => {
|
||||
const peer = await peerManager.createPeer({
|
||||
name: "httpd",
|
||||
gitOptions: gitOptions["bob"],
|
||||
});
|
||||
|
||||
await peer.startNode();
|
||||
await peer.startHttpd();
|
||||
|
||||
await use(peer);
|
||||
},
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
stateDir: async ({}, use, testInfo) => {
|
||||
const stateDir = testInfo.outputDir;
|
||||
await Fs.rm(stateDir, { recursive: true, force: true });
|
||||
await Fs.mkdir(stateDir, { recursive: true });
|
||||
|
||||
await use(stateDir);
|
||||
if (
|
||||
process.env.CI &&
|
||||
(testInfo.status === "passed" || testInfo.status === "skipped")
|
||||
) {
|
||||
await Fs.rm(stateDir, { recursive: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function log(text: string, label: string, outputLog: Stream.Writable) {
|
||||
const output = text
|
||||
.split("\n")
|
||||
.map(line => `${label}${line}`)
|
||||
.join("\n");
|
||||
|
||||
outputLog.write(`${output}\n`);
|
||||
if (!process.env.CI) {
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCobsFixture(
|
||||
peerManager: PeerManager,
|
||||
peer: RadiclePeer,
|
||||
) {
|
||||
await peer.rad(["follow", peer.nodeId, "--alias", "palm"]);
|
||||
await Fs.mkdir(Path.join(tmpDir, "repos", "cobs"), { recursive: true });
|
||||
const { repoFolder, rid, defaultBranch } = await createRepo(peer, {
|
||||
name: "cobs",
|
||||
});
|
||||
const eve = await peerManager.createPeer({
|
||||
name: "eve",
|
||||
gitOptions: gitOptions["eve"],
|
||||
});
|
||||
await eve.startNode({
|
||||
node: { ...defaultConfig.node, connect: [peer.address], alias: "eve" },
|
||||
});
|
||||
await eve.rad(["clone", rid], { cwd: eve.checkoutPath });
|
||||
|
||||
const issueOne = await issue.create(
|
||||
peer,
|
||||
"This `title` has **markdown**",
|
||||
"This is a description\nWith some multiline text.",
|
||||
["bug", "feature-request"],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "react", issueOne, "--emoji", "👍", "--to", issueOne],
|
||||
{
|
||||
cwd: repoFolder,
|
||||
},
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "react", issueOne, "--emoji", "🎉", "--to", issueOne],
|
||||
{
|
||||
cwd: repoFolder,
|
||||
},
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "assign", issueOne, "--add", `did:key:${peer.nodeId}`],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
const { stdout: commentIssueOne } = await peer.rad(
|
||||
[
|
||||
"issue",
|
||||
"comment",
|
||||
issueOne,
|
||||
"--message",
|
||||
"This is a multiline comment\n\nWith some more text.",
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 2),
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "react", issueOne, "--emoji", "🙏", "--to", commentIssueOne],
|
||||
{
|
||||
cwd: repoFolder,
|
||||
},
|
||||
);
|
||||
const { stdout: replyIssueOne } = await peer.rad(
|
||||
[
|
||||
"issue",
|
||||
"comment",
|
||||
issueOne,
|
||||
"--message",
|
||||
"This is a reply, to a first comment.",
|
||||
"--reply-to",
|
||||
commentIssueOne,
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 3),
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "react", issueOne, "--emoji", "🚀", "--to", replyIssueOne],
|
||||
{
|
||||
cwd: repoFolder,
|
||||
},
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"issue",
|
||||
"comment",
|
||||
issueOne,
|
||||
"--message",
|
||||
"A root level comment after a reply, for margins sake.",
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 4),
|
||||
);
|
||||
|
||||
const issueTwo = await issue.create(
|
||||
peer,
|
||||
"A closed issue",
|
||||
"This issue has been closed\n\nsource: [link](https://radicle.xyz)",
|
||||
[],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "state", issueTwo, "--closed"],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
|
||||
const issueThree = await issue.create(
|
||||
peer,
|
||||
"A solved issue",
|
||||
"This issue has been solved\n\n```js\nconsole.log('hello world')\nconsole.log(\"\")\n```",
|
||||
[],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
["issue", "state", issueThree, "--solved"],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
|
||||
const patchOne = await patch.create(
|
||||
peer,
|
||||
["Add README", "This commit adds more information to the README"],
|
||||
"feature/add-readme",
|
||||
() => Fs.writeFile(Path.join(repoFolder, "README.md"), "# Cobs Repo"),
|
||||
["Let's add a README", "This repo needed a README"],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
const { stdout: commentPatchOne } = await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"comment",
|
||||
patchOne,
|
||||
"--message",
|
||||
"I'll review the patch",
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"comment",
|
||||
patchOne,
|
||||
"--message",
|
||||
"Thanks for that!",
|
||||
"--reply-to",
|
||||
commentPatchOne,
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 2),
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"comment",
|
||||
patchOne,
|
||||
"--message",
|
||||
"Yeah no problem!",
|
||||
"--reply-to",
|
||||
commentPatchOne,
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 3),
|
||||
);
|
||||
const { stdout: commentTwo } = await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"comment",
|
||||
patchOne,
|
||||
"--message",
|
||||
"Looking good so far",
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 4),
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"comment",
|
||||
patchOne,
|
||||
"--message",
|
||||
"Thanks again!",
|
||||
"--reply-to",
|
||||
commentTwo,
|
||||
"--quiet",
|
||||
"--no-announce",
|
||||
],
|
||||
createOptions(repoFolder, 5),
|
||||
);
|
||||
await peer.rad(
|
||||
["patch", "review", patchOne, "-m", "LGTM", "--accept"],
|
||||
createOptions(repoFolder, 6),
|
||||
);
|
||||
await patch.merge(
|
||||
peer,
|
||||
defaultBranch,
|
||||
"feature/add-readme",
|
||||
createOptions(repoFolder, 7),
|
||||
);
|
||||
|
||||
const patchTwo = await patch.create(
|
||||
peer,
|
||||
["Add subtitle to README"],
|
||||
"feature/add-more-text",
|
||||
() => Fs.appendFile(Path.join(repoFolder, "README.md"), "\n\n## Subtitle"),
|
||||
[],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"review",
|
||||
patchTwo,
|
||||
"-m",
|
||||
"Not the README we are looking for",
|
||||
"--reject",
|
||||
],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
|
||||
const patchThree = await patch.create(
|
||||
peer,
|
||||
[
|
||||
"Rewrite subtitle to README",
|
||||
"This was really necessary",
|
||||
"Blazingly fast",
|
||||
],
|
||||
"feature/better-subtitle",
|
||||
() => Fs.appendFile(Path.join(repoFolder, "README.md"), "\n\n## Better?"),
|
||||
[
|
||||
"Taking another stab at the README",
|
||||
"This is a big improvement over the last one",
|
||||
"Hopefully **this** is the last time",
|
||||
],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
["patch", "label", patchThree, "--add", "documentation"],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
await eve.rad(
|
||||
["patch", "review", patchThree, "-m", "This looks better"],
|
||||
createOptions(repoFolder, 2),
|
||||
);
|
||||
await Fs.appendFile(
|
||||
Path.join(repoFolder, "README.md"),
|
||||
"\n\nHad to push a new revision",
|
||||
);
|
||||
await peer.git(["add", "."], { cwd: repoFolder });
|
||||
await peer.git(["commit", "-m", "Add more text"], { cwd: repoFolder });
|
||||
await peer.git(
|
||||
[
|
||||
"push",
|
||||
"-o",
|
||||
"patch.message=Most of the missing README text was caused by the git-daemon not having a writers block. It seems like using an RNG was not a good enough solution.",
|
||||
"-o",
|
||||
"patch.message=After this change, the README seem to be written correctly",
|
||||
"rad",
|
||||
"feature/better-subtitle",
|
||||
],
|
||||
createOptions(repoFolder, 3),
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"review",
|
||||
patchThree,
|
||||
"-m",
|
||||
"No this doesn't look better",
|
||||
"--reject",
|
||||
],
|
||||
createOptions(repoFolder, 2),
|
||||
);
|
||||
|
||||
const patchFour = await patch.create(
|
||||
peer,
|
||||
["This patch is going to be archived"],
|
||||
"feature/archived",
|
||||
() => Fs.writeFile(Path.join(repoFolder, "CONTRIBUTING.md"), "# Archived"),
|
||||
[],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
[
|
||||
"patch",
|
||||
"review",
|
||||
patchFour,
|
||||
"-m",
|
||||
"No review due to patch being archived.",
|
||||
],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
await peer.rad(["patch", "archive", patchFour], createOptions(repoFolder, 2));
|
||||
|
||||
const patchFive = await patch.create(
|
||||
peer,
|
||||
["This patch is going to be reverted to draft"],
|
||||
"feature/draft",
|
||||
() => Fs.writeFile(Path.join(repoFolder, "LICENSE"), "Draft"),
|
||||
[],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
await peer.rad(
|
||||
["patch", "ready", patchFive, "--undo"],
|
||||
createOptions(repoFolder, 1),
|
||||
);
|
||||
}
|
||||
|
||||
export async function createMarkdownFixture(peer: RadiclePeer) {
|
||||
await Fs.mkdir(Path.join(tmpDir, "repos", "markdown"), { recursive: true });
|
||||
await execa("tar", [
|
||||
"-xf",
|
||||
Path.join(fixturesDir, "repos", "markdown.tar.bz2"),
|
||||
"-C",
|
||||
Path.join(tmpDir, "repos", "markdown"),
|
||||
]);
|
||||
const { repoFolder } = await createRepo(peer, { name: "markdown" });
|
||||
await Fs.cp(Path.join(tmpDir, "repos", "markdown"), repoFolder, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
await peer.git(["add", "."], { cwd: repoFolder });
|
||||
const commitMessage = `Add Markdown cheat sheet
|
||||
|
||||
Borrowed from [Adam Pritchard][ap].
|
||||
No modifications were made.
|
||||
|
||||
[ap]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet`;
|
||||
await peer.git(["commit", "-m", commitMessage], {
|
||||
cwd: repoFolder,
|
||||
});
|
||||
await peer.git(["push", "rad"], { cwd: repoFolder });
|
||||
await issue.create(
|
||||
peer,
|
||||
"This `title` has **markdown**",
|
||||
'This is a description\n\nWith some multiline text.\n\n```\n23-11-06 10:19 ➜ radicle-jetbrains-plugin git:(main) rad id update --title "Godify jchrist" --description "where jchrist ascends to a god of this project" --delegate did:key:z6MkpaATbhkGbSMysNomYTFVvKG5bnNKYZ2cCamfoHzX9SnL --threshold 1\n\n✓ Identity revision 029837dde8f5c49704e50a19cd709473ac66a456 created\n```',
|
||||
["bug", "feature-request"],
|
||||
{ cwd: repoFolder },
|
||||
);
|
||||
}
|
||||
|
||||
export const aliceMainHead = "7babd25a74eb3752ec24672b5edf0e7ecb4daf24";
|
||||
export const aliceMainCommitMessage =
|
||||
"Verify that crate::DoubleColon::should_work()";
|
||||
export const aliceMainCommitCount = 8;
|
||||
export const aliceRemote =
|
||||
"did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK";
|
||||
export const shortAliceHead = formatOid(aliceMainHead);
|
||||
export const bobRemote =
|
||||
"did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5";
|
||||
export const bobHead = "82f570ec909e77c7e1bb764f1429b1e01b1b4a90";
|
||||
export const bobMainCommitCount = 9;
|
||||
export const shortBobHead = formatOid(bobHead);
|
||||
export const cobRid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe";
|
||||
export const markdownRid = "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg";
|
||||
export const shortNodeRemote = "z6MktU…1xB22S";
|
||||
export const defaultHttpdPort = 8081;
|
||||
export const gitOptions = {
|
||||
alice: {
|
||||
GIT_AUTHOR_NAME: "Alice Liddell",
|
||||
GIT_AUTHOR_EMAIL: "alice@radicle.xyz",
|
||||
GIT_AUTHOR_DATE: "1727621093",
|
||||
GIT_COMMITTER_NAME: "Alice Liddell",
|
||||
GIT_COMMITTER_EMAIL: "alice@radicle.xyz",
|
||||
GIT_COMMITTER_DATE: "1727621093",
|
||||
},
|
||||
bob: {
|
||||
GIT_AUTHOR_NAME: "Bob Belcher",
|
||||
GIT_AUTHOR_EMAIL: "bob@radicle.xyz",
|
||||
GIT_AUTHOR_DATE: "1727621093",
|
||||
GIT_COMMITTER_NAME: "Bob Belcher",
|
||||
GIT_COMMITTER_EMAIL: "bob@radicle.xyz",
|
||||
GIT_COMMITTER_DATE: "1730220293",
|
||||
},
|
||||
|
||||
eve: {
|
||||
GIT_AUTHOR_NAME: "Eve Johnson",
|
||||
GIT_AUTHOR_EMAIL: "eve@radicle.xyz",
|
||||
GIT_AUTHOR_DATE: "1727621093",
|
||||
GIT_COMMITTER_NAME: "Eve Johnson",
|
||||
GIT_COMMITTER_EMAIL: "eve@radicle.xyz",
|
||||
GIT_COMMITTER_DATE: "1730220293",
|
||||
},
|
||||
};
|
||||
export const defaultConfig: Config = {
|
||||
publicExplorer: "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||
preferredSeeds: [],
|
||||
web: {
|
||||
pinned: {
|
||||
repositories: [],
|
||||
},
|
||||
},
|
||||
cli: {
|
||||
hints: true,
|
||||
},
|
||||
node: {
|
||||
alias: "alice",
|
||||
listen: [],
|
||||
peers: {
|
||||
type: "dynamic",
|
||||
},
|
||||
connect: [],
|
||||
externalAddresses: [],
|
||||
network: "main",
|
||||
log: "INFO",
|
||||
relay: "auto",
|
||||
limits: {
|
||||
routingMaxSize: 1000,
|
||||
routingMaxAge: 604800,
|
||||
gossipMaxAge: 1209600,
|
||||
fetchConcurrency: 1,
|
||||
maxOpenFiles: 4096,
|
||||
rate: {
|
||||
inbound: {
|
||||
fillRate: 5.0,
|
||||
capacity: 1024,
|
||||
},
|
||||
outbound: {
|
||||
fillRate: 10.0,
|
||||
capacity: 2048,
|
||||
},
|
||||
},
|
||||
connection: {
|
||||
inbound: 128,
|
||||
outbound: 16,
|
||||
},
|
||||
},
|
||||
workers: 8,
|
||||
seedingPolicy: {
|
||||
default: "block",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
publicExplorer: string;
|
||||
preferredSeeds: string[];
|
||||
cli: { hints: boolean };
|
||||
web: {
|
||||
pinned: {
|
||||
repositories: string[];
|
||||
};
|
||||
imageUrl?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
node: NodeConfig;
|
||||
};
|
||||
|
||||
export type NodeConfig = {
|
||||
alias: string;
|
||||
peers: { type: "static" } | { type: "dynamic" };
|
||||
listen: string[];
|
||||
connect: string[];
|
||||
externalAddresses: string[];
|
||||
proxy?: string;
|
||||
onion?: { mode: "proxy"; address: string } | { mode: "forward" };
|
||||
log: "ERROR" | "WARN" | "INFO" | "DEBUG" | "TRACE";
|
||||
network: "main" | "test";
|
||||
relay: "always" | "never" | "auto";
|
||||
limits: {
|
||||
routingMaxSize: number;
|
||||
routingMaxAge: number;
|
||||
fetchConcurrency: number;
|
||||
gossipMaxAge: number;
|
||||
maxOpenFiles: number;
|
||||
rate: {
|
||||
inbound: {
|
||||
fillRate: number;
|
||||
capacity: number;
|
||||
};
|
||||
outbound: {
|
||||
fillRate: number;
|
||||
capacity: number;
|
||||
};
|
||||
};
|
||||
connection: {
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
};
|
||||
};
|
||||
workers: number;
|
||||
seedingPolicy:
|
||||
| {
|
||||
default: "block";
|
||||
}
|
||||
| {
|
||||
default: "allow";
|
||||
scope: "followed" | "all";
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as Fs from "node:fs";
|
||||
import * as Path from "node:path";
|
||||
import {
|
||||
assertBinariesInstalled,
|
||||
heartwoodRelease,
|
||||
removeWorkspace,
|
||||
tmpDir,
|
||||
} from "@tests/support/support.js";
|
||||
import {
|
||||
defaultConfig,
|
||||
createCobsFixture,
|
||||
createMarkdownFixture,
|
||||
defaultHttpdPort,
|
||||
gitOptions,
|
||||
} from "@tests/support/fixtures.js";
|
||||
import { createPeerManager } from "@tests/support/peerManager.js";
|
||||
|
||||
const heartwoodBinaryPath = Path.join(
|
||||
tmpDir,
|
||||
"bin",
|
||||
"heartwood",
|
||||
heartwoodRelease,
|
||||
);
|
||||
|
||||
process.env.PATH = [heartwoodBinaryPath, process.env.PATH].join(Path.delimiter);
|
||||
|
||||
export default async function globalSetup(): Promise<() => void> {
|
||||
try {
|
||||
await assertBinariesInstalled("rad", heartwoodRelease, heartwoodBinaryPath);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.log("");
|
||||
console.log("To download the required test binaries, run:");
|
||||
console.log(" 👉 ./scripts/install-binaries");
|
||||
console.log("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.SKIP_FIXTURE_CREATION) {
|
||||
console.log(
|
||||
"Recreating static fixtures. Set SKIP_FIXTURE_CREATION to skip this",
|
||||
);
|
||||
await removeWorkspace();
|
||||
}
|
||||
|
||||
const peerManager = await createPeerManager({
|
||||
dataDir: Path.resolve(tmpDir, "peers"),
|
||||
outputLog: Fs.createWriteStream(
|
||||
Path.resolve(tmpDir, "globalPeerManager.log"),
|
||||
)
|
||||
// Workaround for fixing MaxListenersExceededWarning.
|
||||
// Since every prefixOutput stream adds stream listeners that don't autoClose.
|
||||
// TODO: We still seem to have some descriptors left open when running vitest, which we should handle.
|
||||
.setMaxListeners(16),
|
||||
});
|
||||
|
||||
const palm = await peerManager.createPeer({
|
||||
name: "palm",
|
||||
gitOptions: gitOptions["alice"],
|
||||
});
|
||||
|
||||
if (!process.env.SKIP_FIXTURE_CREATION) {
|
||||
await palm.startNode({
|
||||
node: {
|
||||
...defaultConfig.node,
|
||||
seedingPolicy: { default: "allow", scope: "all" },
|
||||
alias: "palm",
|
||||
},
|
||||
});
|
||||
await palm.startHttpd(defaultHttpdPort);
|
||||
|
||||
try {
|
||||
console.log("Creating markdown fixture");
|
||||
await createMarkdownFixture(palm);
|
||||
console.log("Creating cobs fixture");
|
||||
await createCobsFixture(peerManager, palm);
|
||||
console.log("All fixtures created");
|
||||
} catch (error) {
|
||||
console.log("");
|
||||
console.log("Not able to create the required fixtures.");
|
||||
console.log("Make sure you are not using binaries compiled for release.");
|
||||
console.log("");
|
||||
console.log(error);
|
||||
console.log("");
|
||||
process.exit(1);
|
||||
}
|
||||
await palm.stopNode();
|
||||
} else {
|
||||
await palm.startHttpd(defaultHttpdPort);
|
||||
}
|
||||
|
||||
return async () => {
|
||||
await peerManager.shutdown();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ColorName } from "chalk";
|
||||
|
||||
import chalk from "chalk";
|
||||
|
||||
const PADDING_WIDTH = 12;
|
||||
|
||||
// The order here is important, we want successive prefixes to have
|
||||
// high contrast.
|
||||
const availableColors: ColorName[] = [
|
||||
"blue",
|
||||
"yellowBright",
|
||||
"greenBright",
|
||||
"gray",
|
||||
"green",
|
||||
"blueBright",
|
||||
"redBright",
|
||||
"white",
|
||||
"yellow",
|
||||
"red",
|
||||
"magenta",
|
||||
"cyan",
|
||||
];
|
||||
|
||||
let nextColorIndex = 0;
|
||||
|
||||
const assignedColors: Record<string, ColorName> = {};
|
||||
|
||||
export function logPrefix(label: string): string {
|
||||
if (assignedColors[label] === undefined) {
|
||||
const color = availableColors[nextColorIndex];
|
||||
nextColorIndex = (nextColorIndex + 1) % availableColors.length;
|
||||
assignedColors[label] = color;
|
||||
}
|
||||
|
||||
// We reset colors at the beginning of each line to avoid styles from previous
|
||||
// lines messing up prefix colors. This is noticable in rust stack traces
|
||||
// where the `in` and `with` keywords have a white background color.
|
||||
return chalk.reset[assignedColors[label]](
|
||||
`${label.padEnd(PADDING_WIDTH)} | `,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import type * as Execa from "execa";
|
||||
|
||||
import * as Fs from "node:fs/promises";
|
||||
import * as Os from "node:os";
|
||||
import * as Path from "node:path";
|
||||
import * as Stream from "node:stream";
|
||||
import * as Util from "node:util";
|
||||
import * as readline from "node:readline/promises";
|
||||
import getPort from "get-port";
|
||||
import matches from "lodash/matches.js";
|
||||
import waitOn from "wait-on";
|
||||
import { defaultConfig, type Config } from "@tests/support/fixtures.js";
|
||||
import { execa } from "execa";
|
||||
import { logPrefix } from "@tests/support/logPrefix.js";
|
||||
import { randomTag } from "@tests/support/support.js";
|
||||
import { sleep } from "@app/lib/sleep.js";
|
||||
|
||||
export type RefsUpdate =
|
||||
| { updated: { name: string; old: string; new: string } }
|
||||
| { created: { name: string; oid: string } }
|
||||
| { deleted: { name: string; oid: string } }
|
||||
| { skipped: { name: string; oid: string } };
|
||||
|
||||
export type NodeEvent =
|
||||
| {
|
||||
type: "refsFetched";
|
||||
remote: string;
|
||||
rid: string;
|
||||
updated: RefsUpdate[];
|
||||
}
|
||||
| {
|
||||
type: "refsSynced";
|
||||
remote: string;
|
||||
rid: string;
|
||||
}
|
||||
| {
|
||||
type: "seedDiscovered";
|
||||
rid: string;
|
||||
nid: string;
|
||||
}
|
||||
| {
|
||||
type: "seedDropped";
|
||||
nid: string;
|
||||
rid: string;
|
||||
}
|
||||
| {
|
||||
type: "peerConnected";
|
||||
nid: string;
|
||||
}
|
||||
| {
|
||||
type: "peerDisconnected";
|
||||
nid: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export interface RoutingEntry {
|
||||
nid: string;
|
||||
rid: string;
|
||||
}
|
||||
|
||||
interface PeerManagerParams {
|
||||
dataPath: string;
|
||||
radSeed: string;
|
||||
// Name for easy identification. Used on file system and in logs.
|
||||
name: string;
|
||||
gitOptions?: Record<string, string>;
|
||||
outputLog: Stream.Writable;
|
||||
}
|
||||
|
||||
export interface PeerManager {
|
||||
createPeer(params: {
|
||||
name: string;
|
||||
gitOptions?: Record<string, string>;
|
||||
}): Promise<RadiclePeer>;
|
||||
/**
|
||||
* Kill all processes spawned by any of the peers
|
||||
*/
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function createPeerManager(createParams: {
|
||||
dataDir: string;
|
||||
outputLog?: Stream.Writable;
|
||||
}): Promise<PeerManager> {
|
||||
let outputLog: Stream.Writable;
|
||||
let outputLogFile: Fs.FileHandle;
|
||||
if (createParams.outputLog) {
|
||||
outputLog = createParams.outputLog;
|
||||
} else {
|
||||
outputLogFile = await Fs.open(
|
||||
Path.join(createParams.dataDir, "peerManager.log"),
|
||||
"a",
|
||||
);
|
||||
outputLog = outputLogFile.createWriteStream();
|
||||
}
|
||||
|
||||
const peers: RadiclePeer[] = [];
|
||||
return {
|
||||
async createPeer(params) {
|
||||
const peer = await RadiclePeer.create({
|
||||
dataPath: createParams.dataDir,
|
||||
name: params.name,
|
||||
gitOptions: params.gitOptions,
|
||||
radSeed: Array(64)
|
||||
.fill((peers.length + 1).toString())
|
||||
.join(""),
|
||||
outputLog,
|
||||
});
|
||||
peers.push(peer);
|
||||
|
||||
return peer;
|
||||
},
|
||||
async shutdown() {
|
||||
await Promise.all(peers.map(peer => peer.shutdown()));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Specialize the return type of `execa()` to guarantee that `stdout` and
|
||||
// `stderr` are strings.
|
||||
type SpawnResult = Execa.ResultPromise<
|
||||
SpawnOptions & {
|
||||
stdout: (line: unknown) => AsyncGenerator<string, void, void>;
|
||||
stderr: (line: unknown) => AsyncGenerator<string, void, void>;
|
||||
encoding: "utf8";
|
||||
}
|
||||
>;
|
||||
|
||||
type SpawnOptions = Omit<
|
||||
Execa.Options,
|
||||
"stdin" | "stdout" | "stderr" | "lines" | "encoding"
|
||||
>;
|
||||
|
||||
export interface BaseUrl {
|
||||
hostname: string;
|
||||
port: number;
|
||||
scheme: string;
|
||||
}
|
||||
|
||||
export class RadiclePeer {
|
||||
public checkoutPath: string;
|
||||
public nodeId: string;
|
||||
|
||||
#radSeed: string;
|
||||
#socket: string;
|
||||
#radHome: string;
|
||||
#eventRecords: NodeEvent[] = [];
|
||||
#outputLog: Stream.Writable;
|
||||
#gitOptions?: Record<string, string>;
|
||||
#listenSocketAddr?: string;
|
||||
#httpdBaseUrl?: BaseUrl;
|
||||
#nodeProcess?: SpawnResult;
|
||||
// Name for easy identification. Used on file system and in logs.
|
||||
#name: string;
|
||||
#childProcesses: SpawnResult[] = [];
|
||||
|
||||
private constructor(props: {
|
||||
checkoutPath: string;
|
||||
nodeId: string;
|
||||
radSeed: string;
|
||||
socket: string;
|
||||
gitOptions?: Record<string, string>;
|
||||
radHome: string;
|
||||
logFile: Stream.Writable;
|
||||
name: string;
|
||||
}) {
|
||||
this.checkoutPath = props.checkoutPath;
|
||||
this.nodeId = props.nodeId;
|
||||
this.#gitOptions = props.gitOptions;
|
||||
this.#radSeed = props.radSeed;
|
||||
this.#socket = props.socket;
|
||||
this.#radHome = props.radHome;
|
||||
this.#outputLog = props.logFile;
|
||||
this.#name = props.name;
|
||||
}
|
||||
|
||||
public async waitForEvent(searchEvent: NodeEvent, timeoutInMs: number) {
|
||||
const start = new Date().getTime();
|
||||
|
||||
while (true) {
|
||||
if (this.#eventRecords.find(matches(searchEvent))) {
|
||||
return;
|
||||
}
|
||||
if (new Date().getTime() - start > timeoutInMs) {
|
||||
throw Error(
|
||||
`Timeout waiting for event on node ${this.#name} ${Util.inspect(
|
||||
searchEvent,
|
||||
{ depth: null },
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
public static async create({
|
||||
dataPath,
|
||||
name,
|
||||
gitOptions,
|
||||
radSeed: node,
|
||||
outputLog: logFile,
|
||||
}: PeerManagerParams): Promise<RadiclePeer> {
|
||||
const checkoutPath = Path.join(dataPath, name, "copy");
|
||||
await Fs.mkdir(checkoutPath, { recursive: true });
|
||||
const radHome = Path.join(dataPath, name, "home");
|
||||
await Fs.mkdir(radHome, { recursive: true });
|
||||
|
||||
const socketDir = await Fs.mkdtemp(
|
||||
Path.join(Os.tmpdir(), `radicle-${randomTag()}`),
|
||||
);
|
||||
const socket = Path.join(socketDir, "control.sock");
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const env = {
|
||||
...gitOptions,
|
||||
RAD_HOME: radHome,
|
||||
RAD_PASSPHRASE: "asdf",
|
||||
RAD_KEYGEN_SEED: node,
|
||||
RAD_SOCKET: socket,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
await execa("rad", ["auth", "--alias", name], { env });
|
||||
const { stdout: nodeId } = await execa("rad", ["self", "--nid"], { env });
|
||||
|
||||
return new RadiclePeer({
|
||||
checkoutPath,
|
||||
gitOptions,
|
||||
radSeed: node,
|
||||
socket,
|
||||
nodeId,
|
||||
radHome,
|
||||
logFile,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
public async startHttpd(port?: number): Promise<void> {
|
||||
if (!port) {
|
||||
port = await getPort();
|
||||
}
|
||||
this.#httpdBaseUrl = {
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
scheme: "http",
|
||||
};
|
||||
void this.spawn("cargo", [
|
||||
"run",
|
||||
"--manifest-path",
|
||||
"./crates/test-http-api/Cargo.toml",
|
||||
"--",
|
||||
"--listen",
|
||||
`${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`,
|
||||
]);
|
||||
|
||||
await waitOn({
|
||||
resources: [
|
||||
`tcp:${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`,
|
||||
],
|
||||
timeout: 20_000,
|
||||
});
|
||||
}
|
||||
|
||||
public async startNode(config: Partial<Config> = defaultConfig) {
|
||||
const listenPort = await getPort();
|
||||
this.#listenSocketAddr = `0.0.0.0:${listenPort}`;
|
||||
|
||||
await updateConfig(this.#radHome, config);
|
||||
|
||||
this.#nodeProcess = this.spawn("radicle-node", [
|
||||
"--listen",
|
||||
this.#listenSocketAddr,
|
||||
]);
|
||||
|
||||
await waitOn({
|
||||
resources: [`socket:${this.#socket}`],
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
const { stdout } = this.rad(["node", "events"], {
|
||||
cwd: this.#radHome,
|
||||
});
|
||||
|
||||
if (!stdout) {
|
||||
throw new Error("Could not get stdout to track events");
|
||||
}
|
||||
|
||||
readline
|
||||
.createInterface({
|
||||
input: stdout,
|
||||
terminal: false,
|
||||
})
|
||||
.on("line", line => {
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch {
|
||||
console.log("Error parsing event", line);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#eventRecords.push(event);
|
||||
for (const line of Util.inspect(event, { depth: null }).split("\n")) {
|
||||
this.#outputLog.write(
|
||||
`${logPrefix(`${this.#name} node events`)} ${line}\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async stopNode() {
|
||||
// Don’t leak unhandled rejections when forcefully killing the process
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
this.#nodeProcess?.catch(() => {});
|
||||
this.#nodeProcess?.kill("SIGTERM");
|
||||
|
||||
await waitOn({
|
||||
resources: [`socket:${this.#socket}`],
|
||||
reverse: true,
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all child processes created with `spawn()`, including the node and
|
||||
* httpd processes.
|
||||
*/
|
||||
public async shutdown() {
|
||||
// We don’t care about proper cleanup. We just want to make sure that no
|
||||
// processes are running anymore.
|
||||
this.#childProcesses.forEach(p => {
|
||||
// Don’t leak unhandled rejections when forcefully killing the process
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
p.catch(() => {});
|
||||
p.kill("SIGKILL");
|
||||
});
|
||||
}
|
||||
|
||||
public get address(): string {
|
||||
if (!this.#listenSocketAddr) {
|
||||
throw new Error("Remote node has no listen addr yet");
|
||||
}
|
||||
return `${this.nodeId}@${this.#listenSocketAddr}`;
|
||||
}
|
||||
|
||||
public uiUrl(): string {
|
||||
if (!this.#httpdBaseUrl) {
|
||||
throw new Error("No httpd service running");
|
||||
}
|
||||
|
||||
return `/nodes/${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`;
|
||||
}
|
||||
|
||||
public ridUrl(rid: string): string {
|
||||
return `/nodes/${this.httpdBaseUrl.hostname}:${this.httpdBaseUrl.port}/${rid}`;
|
||||
}
|
||||
|
||||
public get httpdBaseUrl(): BaseUrl {
|
||||
if (!this.#httpdBaseUrl) {
|
||||
throw new Error("No httpd service running");
|
||||
}
|
||||
|
||||
return this.#httpdBaseUrl;
|
||||
}
|
||||
|
||||
public git(args: string[] = [], opts?: SpawnOptions): SpawnResult {
|
||||
return this.spawn("git", args, { ...opts });
|
||||
}
|
||||
|
||||
public rad(args: string[] = [], opts?: SpawnOptions): SpawnResult {
|
||||
return this.spawn("rad", args, { ...opts });
|
||||
}
|
||||
|
||||
public spawn(
|
||||
cmd: string,
|
||||
args: string[] = [],
|
||||
opts?: SpawnOptions,
|
||||
): SpawnResult {
|
||||
const prefix = logPrefix(`${this.#name} ${cmd}`);
|
||||
const outputLog = this.#outputLog;
|
||||
|
||||
function* logWithPrefix(line: unknown) {
|
||||
if (typeof line === "string") {
|
||||
outputLog.write(`${prefix} ${line}\n`, "utf8");
|
||||
}
|
||||
yield line;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const childProcess = execa(cmd, args, {
|
||||
...opts,
|
||||
env: {
|
||||
GIT_CONFIG_GLOBAL: "/dev/null",
|
||||
GIT_CONFIG_NOSYSTEM: "1",
|
||||
RAD_HOME: this.#radHome,
|
||||
RAD_PASSPHRASE: "asdf",
|
||||
RAD_LOCAL_TIME: "1671125284",
|
||||
RAD_KEYGEN_SEED: this.#radSeed,
|
||||
RAD_SOCKET: this.#socket,
|
||||
...opts?.env,
|
||||
...this.#gitOptions,
|
||||
},
|
||||
encoding: "utf8",
|
||||
stdout: logWithPrefix,
|
||||
stderr: logWithPrefix,
|
||||
});
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
this.#childProcesses.push(childProcess);
|
||||
|
||||
return childProcess;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateConfig(radHome: string, configParams: Partial<Config>) {
|
||||
const configPath = Path.join(radHome, "config.json");
|
||||
const configFile = await Fs.readFile(configPath, "utf-8");
|
||||
const config = {
|
||||
defaultConfig,
|
||||
...JSON.parse(configFile),
|
||||
};
|
||||
config.preferredSeeds = [];
|
||||
config.web = { ...config.web, ...configParams.web };
|
||||
config.node = {
|
||||
...config.node,
|
||||
...configParams.node,
|
||||
network: "test",
|
||||
};
|
||||
await Fs.writeFile(configPath, JSON.stringify(config), "utf-8");
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { RadiclePeer } from "@tests/support/peerManager";
|
||||
|
||||
import * as Path from "node:path";
|
||||
|
||||
export async function changeBranch(peer: string, branch: string, page: Page) {
|
||||
await page.getByTitle("Change branch").click();
|
||||
const peerLocator = page.getByLabel("peer-item").filter({ hasText: peer });
|
||||
await peerLocator.getByTitle("Expand peer").click();
|
||||
await page.getByRole("button", { name: branch }).click();
|
||||
}
|
||||
|
||||
// Create a repo using the rad CLI.
|
||||
export async function createRepo(
|
||||
peer: RadiclePeer,
|
||||
{
|
||||
name,
|
||||
description = "",
|
||||
defaultBranch = "main",
|
||||
visibility = "public",
|
||||
}: {
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultBranch?: string;
|
||||
visibility?: "public" | "private";
|
||||
},
|
||||
): Promise<{ rid: string; repoFolder: string; defaultBranch: string }> {
|
||||
const repoFolder = Path.join(peer.checkoutPath, name);
|
||||
|
||||
await peer.git(["init", name, "--initial-branch", defaultBranch], {
|
||||
cwd: peer.checkoutPath,
|
||||
});
|
||||
await peer.git(["commit", "--allow-empty", "--message", "initial commit"], {
|
||||
cwd: repoFolder,
|
||||
});
|
||||
await peer.rad(
|
||||
[
|
||||
"init",
|
||||
"--name",
|
||||
name,
|
||||
"--default-branch",
|
||||
defaultBranch,
|
||||
"--description",
|
||||
description,
|
||||
`--${visibility}`,
|
||||
],
|
||||
{
|
||||
cwd: repoFolder,
|
||||
},
|
||||
);
|
||||
|
||||
const { stdout: rid } = await peer.rad(["inspect"], {
|
||||
cwd: repoFolder,
|
||||
});
|
||||
|
||||
return { rid, repoFolder, defaultBranch };
|
||||
}
|
||||
|
||||
export function extractPatchId(cmdOutput: { stderr: string }) {
|
||||
const match = cmdOutput.stderr.match(/[0-9a-f]{40}/);
|
||||
if (match) {
|
||||
return match[0];
|
||||
} else {
|
||||
throw new Error("Could not get patch id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@tests/support/fixtures.js";
|
||||
|
||||
// Reloads the current page and verifies that the URL stays correct
|
||||
export const expectUrlPersistsReload = async (page: Page) => {
|
||||
const url = page.url();
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(url);
|
||||
};
|
||||
|
||||
// Navigates back, checks the URL and navigates forward back to the initial page
|
||||
export const expectBackAndForwardNavigationWorks = async (
|
||||
beforeURL: string,
|
||||
page: Page,
|
||||
) => {
|
||||
const currentURL = page.url();
|
||||
|
||||
await page.goBack();
|
||||
await page
|
||||
.getByRole("progressbar", { name: "Page loading" })
|
||||
.waitFor({ state: "hidden" });
|
||||
await expect(page).toHaveURL(beforeURL);
|
||||
await page.goForward();
|
||||
|
||||
await page
|
||||
.getByRole("progressbar", { name: "Page loading" })
|
||||
.waitFor({ state: "hidden" });
|
||||
await expect(page).toHaveURL(currentURL);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Options } from "execa";
|
||||
|
||||
import { execa } from "execa";
|
||||
import * as Crypto from "node:crypto";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as Path from "node:path";
|
||||
import * as Fs from "node:fs/promises";
|
||||
|
||||
// Generate string of 12 random characters with 8 bits of entropy.
|
||||
export function randomTag(): string {
|
||||
return Crypto.randomBytes(8).toString("hex");
|
||||
}
|
||||
|
||||
export function createOptions(repoFolder: string, days: number): Options {
|
||||
return {
|
||||
cwd: repoFolder,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
env: { RAD_LOCAL_TIME: (1671211684 + days * 86400).toString() },
|
||||
};
|
||||
}
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
export const supportDir = Path.dirname(filename);
|
||||
export const tmpDir = Path.resolve(supportDir, "..", "./tmp");
|
||||
export const fixturesDir = Path.resolve(supportDir, "..", "./fixtures");
|
||||
const workspacePaths = [Path.join(tmpDir, "peers"), Path.join(tmpDir, "repos")];
|
||||
|
||||
export const heartwoodRelease = await Fs.readFile(
|
||||
`${supportDir}/heartwood-release`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// Assert that binaries are installed and are the correct version.
|
||||
export async function assertBinariesInstalled(
|
||||
binary: string,
|
||||
expectedVersion: string,
|
||||
expectedPath: string,
|
||||
): Promise<void> {
|
||||
const { stdout: which } = await execa("which", [binary]);
|
||||
if (Path.dirname(which) !== expectedPath) {
|
||||
throw new Error(
|
||||
`${binary} path doesn't match used ${binary} binary: ${expectedPath} !== ${which}`,
|
||||
);
|
||||
}
|
||||
const { stdout: version } = await execa(binary, ["--version"]);
|
||||
if (!version.includes(expectedVersion)) {
|
||||
throw new Error(
|
||||
`${binary} version ${version} does not satisfy ${expectedVersion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeWorkspace(): Promise<void> {
|
||||
for (const path of workspacePaths) {
|
||||
await Fs.rm(path, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"include": ["src", "./*.js", "./*.ts"],
|
||||
"include": ["src", "tests", "./*.js", "./*.ts"],
|
||||
"exclude": ["node_modules/*", "isolation/*"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
@@ -21,7 +21,8 @@
|
||||
"paths": {
|
||||
"@app/*": ["./src/*"],
|
||||
"@bindings/*": ["./crates/radicle-types/bindings/*"],
|
||||
"@public/*": ["./public/*"]
|
||||
"@public/*": ["./public/*"],
|
||||
"@tests/*": ["./tests/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user