Replace Cypress with Playwright

Playwright is more flexible and has better developer experience than Cypress.
We add end to end tests covering:

  - project source browsing
    - tree view
    - commit list view
    - commit view
  - routing
    - hash based
    - history API based
  - clipboard copy paste functionality
  - seed view
  - project search
  - landing page view
  - settings

Co-authored-by: Sebastian Martinez <me@sebastinez.dev>
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2022-12-01 15:54:56 +01:00
co-authored by Sebastian Martinez
parent d740d7e5a1
commit 9cb9332e88
83 changed files with 2173 additions and 4148 deletions
+20 -14
View File
@@ -3,19 +3,25 @@ on: [push, pull_request]
jobs:
check-build:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '18.12.1'
- uses: actions/checkout@v2
- run: npm ci
- run: npm run build
- name: Cypress run
uses: cypress-io/github-action@v4
with:
browser: chrome
start: npm run serve
wait-on: "http://localhost:4173"
command: npx cypress run --config baseUrl="http://localhost:4173" --spec 'cypress/e2e/home.spec.ts'
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.12.1'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Build app
run: npm run build
- name: Run Playwright build smoke test
run: npm run test:e2e -- --project chromium --config playwright.buildSmoke.config.ts tests/e2e/buildSmoke.spec.ts
- uses: actions/upload-artifact@v3
if: always()
with:
name: test-artifacts-${{ runner.os }}
retention-days: 30
path: |
tests/artifacts/**/*
+30
View File
@@ -0,0 +1,30 @@
name: check-e2e
on: [push, pull_request]
jobs:
check-e2e:
strategy:
matrix:
browser: [chromium, firefox]
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.12.1'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Start http-api test server
run: ./scripts/run-http-api-with-fixtures --non-interactive --detach
- name: Run Playwright tests
run: npm run test:e2e -- --project ${{ matrix.browser }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: test-artifacts-${{ runner.os }}
retention-days: 30
path: |
tests/artifacts/**/*
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '18.12.1'
- uses: actions/checkout@v2
@@ -1,21 +0,0 @@
name: check-integration-test
on: [push, pull_request]
jobs:
chrome:
runs-on: ubuntu-latest
name: E2E on Chrome
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '18.12.1'
- name: Checkout
uses: actions/checkout@v2
- name: Cypress run
uses: cypress-io/github-action@v4
with:
browser: chrome
start: npm start
wait-on: 'http://localhost:3000'
command: npm run test:e2e
+3 -4
View File
@@ -2,15 +2,14 @@ name: check-unit-test
on: [push, pull_request]
jobs:
check:
check-unit-test:
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '18.12.1'
- name: Checkout
uses: actions/checkout@v2
- run: npm ci
- run: ./scripts/unit-test
shell: bash
- run: npm run test:unit
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '18.12.1'
- uses: actions/checkout@v2
+2 -1
View File
@@ -12,7 +12,8 @@ KaTeX_**.woff2
public/twemoji/*.svg
# Integration Tests
cypress/screenshots
tests/tmp/**/*
tests/artifacts/**/*
# Mac OS
.DS_Store
-21
View File
@@ -1,21 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
video: false,
defaultCommandTimeout: 10000,
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
specPattern: "cypress/e2e/**/*spec.ts",
baseUrl: "http://localhost:3000",
},
component: {
devServer: {
framework: "svelte",
bundler: "vite",
},
specPattern: "src/**/*spec.ts",
},
});
-31
View File
@@ -1,31 +0,0 @@
/// <reference types="cypress" />
import { MockProvider } from "@rsksmart/mock-web3-provider";
describe("landing page", () => {
it("displays correctly projects", () => {
cy.intercept(
{ pathname: "/v1/projects/*" },
{ fixture: "projectInfo.json" },
).as("projectInfo");
cy.intercept(
{ pathname: "/v1/projects/*/activity" },
{ fixture: "projectActivity.json" },
).as("projectActivity");
cy.visit("/", {
onBeforeLoad(win) {
const address = "0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D";
const privateKey =
"de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3";
win.ethereum = new MockProvider({
address,
privateKey,
networkVersion: 1,
});
},
});
cy.wait(["@projectInfo", "@projectActivity"]);
cy.get(".project .name")
.first()
.should("have.text", "bright-forest-protocol");
});
});
-174
View File
@@ -1,174 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/// <reference types="cypress" />
import { MockProvider } from "@rsksmart/mock-web3-provider";
import { getPath } from "../support/e2e";
const groupedCommits = [
{
groupDate: "Thursday, March 3, 2022",
commits: [
{
header: {
sha: "9cd3532",
summary: "Second commit",
description: "",
committer: {
name: "dabit3",
mail: "dabit3@gmail.com",
},
committerTime: "06:51 GMT+1",
},
},
],
},
{
groupDate: "Wednesday, March 2, 2022",
commits: [
{
header: {
sha: "e045b92",
summary: "Update README",
description: "",
committer: {
name: "dabit3",
mail: "dabit3@gmail.com",
},
committerTime: "17:14 GMT+1",
},
},
{
header: {
sha: "cbf5df4",
summary: "initial commit",
description: "this is the first commit of many",
committer: {
name: "dabit3",
mail: "dabit3@gmail.com",
},
committerTime: "16:58 GMT+1",
},
},
],
},
];
describe("project commits", () => {
beforeEach(() => {
cy.intercept("/", {
fixture: "projectHome.json",
}).as("seedHome");
cy.intercept("v1/peer", {
fixture: "projectPeer.json",
}).as("seedPeer");
cy.intercept("v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy", {
fixture: "projectInfo.json",
}).as("seedInfo");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes",
{ fixture: "projectRemotes.json" },
).as("seedRemotes");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/tree/56e4e029c294b08546386e1fb706b772c7433c49/",
{ fixture: "projectTree56e4e02.json" },
).as("seedTree56e4e02");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/tree/cbf5df499ab4f4a908f1756fbe2c236a4530516a/",
{ fixture: "projectTreecbf5df4.json" },
).as("seedTreecbf5df4");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/commits?parent=56e4e029c294b08546386e1fb706b772c7433c49&verified=true",
{ fixture: "projectCommits.json" },
).as("seedCommits");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/commits/cbf5df499ab4f4a908f1756fbe2c236a4530516a",
{ fixture: "projectCommit.json" },
).as("seedCommit");
});
it("display commit groups and commit trailers", () => {
cy.visit(
"/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/history",
{
onBeforeLoad(win) {
const address = "0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D";
const privateKey =
"de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3";
win.ethereum = new MockProvider({
address,
privateKey,
networkVersion: 1,
});
},
},
);
cy.wait([
"@seedHome",
"@seedPeer",
"@seedInfo",
"@seedRemotes",
"@seedTree56e4e02",
"@seedCommits",
]);
// This iterates over the commit groups and then over each commit.
cy.get(".commit-group")
.should("have.length", 2)
.each((item, index) => {
expect(Cypress.$(item.children(".commit-date")).text()).to.eq(
groupedCommits[index].groupDate,
);
const $el = Cypress.$(item.find(".commit-teaser"));
cy.wrap($el).each((commit, commitIndex) => {
expect(Cypress.$(commit).find(".hash").text()).to.eq(
groupedCommits[index].commits[commitIndex].header.sha,
);
expect(Cypress.$(commit).find(".summary").text()).to.eq(
groupedCommits[index].commits[commitIndex].header.summary,
);
expect(Cypress.$(commit).find(".committer").first().text()).to.eq(
groupedCommits[index].commits[commitIndex].header.committer.name,
);
});
});
cy.get(".commit-teaser .badge").last().trigger("mouseenter");
// Checking that the initial commit has the Verified badge
cy.get(".popup .header").should(
"have.text",
"✔ This commit was signed\n with the committer's radicle key.",
);
cy.get(".popup .peer").should(
"contain.text",
"hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
);
cy.get(".popup .committer").should("contain.text", "dabit3");
cy.get(".commit").last().click();
});
it("display commit details", () => {
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/commits/cbf5df499ab4f4a908f1756fbe2c236a4530516a",
);
});
cy.get("header .summary .txt-medium").should("have.text", "initial commit");
cy.get(".commit pre.description").should(
"have.text",
"this is the first commit of many",
);
cy.get("header .committer").should("have.text", "dabit3");
cy.get("div.changeset-summary").should(
"have.text",
"1 file(s) changed, 1 file(s) created, 1 file(s) deleted\n with\n 0 addition(s)\n and\n 0 deletion(s)",
);
cy.get("header.file-header:nth-child(1) p")
.first()
.should("have.text", "test.md")
.next()
.should("have.text", "created");
cy.get("tr.diff-line td.diff-line-number").contains("16");
cy.get("tr.diff-line td.diff-line-content").contains(
"To prevent front-running, the RAD/USDC balances are set through the Uniswap router *proxy* contract",
);
});
});
-181
View File
@@ -1,181 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/// <reference types="cypress" />
import { MockProvider } from "@rsksmart/mock-web3-provider";
import { getPath } from "../support/e2e";
describe("project header", () => {
beforeEach(() => {
cy.intercept("/", {
fixture: "projectHome.json",
}).as("projectHome");
cy.intercept("v1/peer", {
fixture: "projectPeer.json",
}).as("projectPeer");
cy.intercept("v1/projects/bright-forest-protocol", {
fixture: "projectInfo.json",
}).as("projectInfo");
cy.intercept("v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy", {
fixture: "projectInfo.json",
}).as("projectInfo");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes",
{ fixture: "projectRemotes.json" },
).as("projectRemotes");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/issues",
{ fixture: "projectIssues.json" },
).as("projectIssues");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/patches",
{ fixture: "projectPatches.json" },
).as("projectPatches");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/tree/56e4e029c294b08546386e1fb706b772c7433c49",
{ fixture: "projectTree56e4e02.json" },
).as("projectTree56e4e02");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/tree/cbf5df499ab4f4a908f1756fbe2c236a4530516a",
{ fixture: "projectTreecbf5df4.json" },
).as("projectTreecbf5df4");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke",
{ fixture: "projectBranches.json" },
).as("projectBranches");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/readme/56e4e029c294b08546386e1fb706b772c7433c49",
{ fixture: "projectReadme.json" },
).as("projectReadme");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/commits?parent=cbf5df499ab4f4a908f1756fbe2c236a4530516a?verified=true",
{ fixture: "projectCommits.json" },
).as("projectCommits");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/readme/cbf5df499ab4f4a908f1756fbe2c236a4530516a",
{ fixture: "projectReadme.json" },
).as("projectReadme");
cy.intercept(
"v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/commit/cbf5df499ab4f4a908f1756fbe2c236a4530516a",
{ fixture: "projectCommit.json" },
).as("projectCommit");
});
it("renders header correctly", () => {
cy.visit("/seeds/willow.radicle.garden/bright-forest-protocol", {
onBeforeLoad(win) {
const address = "0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D";
const privateKey =
"de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3";
win.ethereum = new MockProvider({
address,
privateKey,
networkVersion: 1,
});
},
});
cy.wait([
"@projectHome",
"@projectPeer",
"@projectInfo",
"@projectRemotes",
"@projectTree56e4e02",
"@projectReadme",
]);
cy.get('[aria-label="Seed"] span').should(
"have.text",
"willow.radicle.garden",
);
cy.get('[aria-label="Commit count"]').should(
"have.text",
"3\n commit(s)",
);
cy.get('[aria-label="Contributor count"]').should(
"have.text",
"1\n contributor(s)",
);
cy.get("div.stat.branch")
.should("have.class", "not-allowed")
.should("have.text", "main");
cy.get("div.hash.layout-desktop").should("have.text", "56e4e02");
cy.get("div.clone-button").click();
});
it("lets user change peer", () => {
cy.get("div.selector").click();
cy.get("div.dropdown-item")
.contains(
"dabit3 hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke delegate",
)
.click();
cy.wait([
"@projectHome",
"@projectPeer",
"@projectInfo",
"@projectRemotes",
"@projectBranches",
"@projectTree56e4e02",
"@projectReadme",
]);
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/bright-forest-protocol/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/tree",
);
});
cy.get(
"span.peer-id span[title='hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke']",
).should("have.text", "hyndc7…j9oeke");
cy.get("div.stat.peer span.peer-id").should("have.text", "dabit3");
cy.get("div.stat.peer span.badge").should("have.text", "delegate");
});
it("lets user on a specific peer change branches", () => {
cy.get("div.commit div.stat.branch").click();
cy.get("div.dropdown-item")
.first()
.contains("main")
.next()
.contains("master")
.click();
cy.wait(["@projectTreecbf5df4", "@projectReadme"]);
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/bright-forest-protocol/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/tree/master",
);
});
cy.get("div.stat.branch").should("have.text", "master");
cy.get("div.hash.layout-desktop").should("have.text", "cbf5df4");
});
it("navigate to commit history", () => {
cy.get('[aria-label="Commit count"]')
.should("not.have.class", "active")
.click();
cy.wait(["@projectCommits"]);
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/history/master",
);
});
cy.get('[aria-label="Commit count"]').should("have.class", "active");
});
it("navigate to issues listing", () => {
cy.get('[aria-label="Issue count"]').click();
cy.wait(["@projectIssues"]);
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/issues",
);
});
cy.get('[aria-label="Issue count"]').should("have.class", "active");
});
it("navigate to patches listing", () => {
cy.get('[aria-label="Patch count"]').click();
cy.wait(["@projectPatches"]);
cy.location().should(location => {
expect(getPath(location)).to.eq(
"/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/patches",
);
});
cy.get('[aria-label="Patch count"]').should("have.class", "active");
});
});
-56
View File
@@ -1,56 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/// <reference types="cypress" />
import { MockProvider } from "@rsksmart/mock-web3-provider";
describe("project meta", () => {
it("displays the correct project information", () => {
cy.intercept("https://willow.radicle.garden:8777/", {
fixture: "projectHome.json",
}).as("projectHome");
cy.intercept("https://willow.radicle.garden:8777/v1/peer", {
fixture: "projectPeer.json",
}).as("projectPeer");
cy.intercept(
"https://willow.radicle.garden:8777/v1/projects/bright-forest-protocol",
{ fixture: "projectInfo.json" },
).as("projectInfo");
cy.intercept(
"https://willow.radicle.garden:8777/v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes",
{ fixture: "projectRemotes.json" },
).as("projectRemotes");
cy.intercept(
"https://willow.radicle.garden:8777/v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/tree/56e4e029c294b08546386e1fb706b772c7433c49",
{ fixture: "projectTree56e4e02.json" },
).as("projectTree56e4e02");
cy.intercept(
"https://willow.radicle.garden:8777/v1/projects/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/readme/56e4e029c294b08546386e1fb706b772c7433c49",
{ fixture: "projectReadme.json" },
).as("projectReadme");
cy.visit("/seeds/willow.radicle.garden/bright-forest-protocol", {
onBeforeLoad(win) {
const address = "0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D";
const privateKey =
"de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3";
win.ethereum = new MockProvider({
address,
privateKey,
networkVersion: 1,
});
},
});
cy.wait([
"@projectHome",
"@projectPeer",
"@projectInfo",
"@projectRemotes",
"@projectTree56e4e02",
]);
cy.get("div.title").contains("bright-forest-protocol");
cy.get("div.urn").contains("rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy");
cy.get("div.description").contains("bfc-sc");
cy.get("div.column-right article div.markdown h1").should(
"have.text",
"Basic Sample Hardhat Project",
);
});
});
-48
View File
@@ -1,48 +0,0 @@
[
{
"groupDate": "Thursday, March 3, 2022",
"commits": [
{
"header": {
"sha": "9cd3532",
"summary": "Second commit",
"description": "",
"committer": {
"name": "dabit3",
"mail": "dabit3@gmail.com"
},
"committerTime": "06:51 GMT+1"
}
}
]
},
{
"groupDate": "Wednesday, March 2, 2022",
"commits": [
{
"header": {
"sha": "e045b92",
"summary": "Update README",
"description": "",
"committer": {
"name": "dabit3",
"mail": "dabit3@gmail.com"
},
"committerTime": "17:14 GMT+1"
}
},
{
"header": {
"sha": "cbf5df4",
"summary": "initial commit",
"description": "this is the first commit of many",
"committer": {
"name": "dabit3",
"mail": "dabit3@gmail.com"
},
"committerTime": "16:58 GMT+1"
}
}
]
}
]
-3
View File
@@ -1,3 +0,0 @@
{
"activity": []
}
-6
View File
@@ -1,6 +0,0 @@
{
"heads": {
"master": "cbf5df499ab4f4a908f1756fbe2c236a4530516a",
"main": "56e4e029c294b08546386e1fb706b772c7433c49"
}
}
-146
View File
@@ -1,146 +0,0 @@
{
"header": {
"sha1": "cbf5df499ab4f4a908f1756fbe2c236a4530516a",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "initial commit",
"description": "this is the first commit of many",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646236685
},
"stats": {
"additions": 0,
"deletions": 0
},
"diff": {
"created": [
{
"path": "test.md",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -0,0 +1 @@\n",
"lines": [
{
"type": "addition",
"line": "Hello world\n",
"lineNum": 1
}
]
}
]
}
}
],
"deleted": [
{
"path": "test.md",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -0,0 +1 @@\n",
"lines": [
{
"type": "addition",
"line": "Hello world\n",
"lineNum": 1
}
]
}
]
}
}
],
"moved": [],
"copied": [],
"modified": [
{
"path": "foo.bar",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -13,7 +13,11 @@ If executed, this proposal will:\n",
"lines": [
{
"type": "context",
"line": "\n",
"lineNumOld": 13,
"lineNumNew": 13
},
{
"type": "context",
"line": "After execution, the Timelock holds all Uniswap LP tokens for the RAD/USDC pair.\n",
"lineNumOld": 14,
"lineNumNew": 14
},
{
"type": "context",
"line": "\n",
"lineNumOld": 15,
"lineNumNew": 15
},
{
"type": "deletion",
"line": "To prevent front-running, the RAD/USDC balances are set through the Uniswap router *proxy* contract, deployed at `0xB76FC4EbE4fC0CC34AF440Ad79565A68Bfcb095e`. Only the Radicle Foundation can set these balances, via the `setLiquidity` function. This contract function must be called as close as possible to the execution of this proposal, to provide liquidity at the correct market price.\n",
"lineNum": 16
},
{
"type": "addition",
"line": "To prevent front-running, the RAD/USDC balances are set through the Uniswap\n",
"lineNum": 16
},
{
"type": "addition",
"line": "router *proxy* contract, deployed at `0xB76FC4EbE4fC0CC34AF440Ad79565A68Bfcb095e`.\n",
"lineNum": 17
},
{
"type": "addition",
"line": "Only the Radicle Foundation can set these balances, via the `setLiquidity`\n",
"lineNum": 18
},
{
"type": "addition",
"line": "function. This contract function must be called as close as possible to the\n",
"lineNum": 19
},
{
"type": "addition",
"line": "execution of this proposal, to provide liquidity at the correct market price.\n",
"lineNum": 20
},
{
"type": "context",
"line": "\n",
"lineNumOld": 17,
"lineNumNew": 21
},
{
"type": "context",
"line": "## Notes\n",
"lineNumOld": 18,
"lineNumNew": 22
},
{
"type": "context",
"line": "\n",
"lineNumOld": 19,
"lineNumNew": 23
}
]
}
]
}
}
]
},
"branches": ["main", "master"]
}
-74
View File
@@ -1,74 +0,0 @@
{
"headers": [
{
"header": {
"sha1": "cbf5df499ab4f4a908f1756fbe2c236a4530516a",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "initial commit",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646236685
},
"context": {
"committer": {
"peer": {
"id": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"person": {
"name": "dabit3"
},
"delegate": true
}
}
}
},
{
"header": {
"sha1": "e045b92297d21a709f3dea84d692c482ee61f6b2",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "Update README",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646237685
},
"context": {
"committer": null
}
},
{
"header": {
"sha1": "9cd353282bec77fef5dd56227173fc675d2363ce",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "Second commit",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646286685
},
"context": {
"committer": null
}
}
],
"stats": {
"commits": 3,
"branches": 1,
"contributors": 1
}
}
-23
View File
@@ -1,23 +0,0 @@
{
"message": "Welcome!",
"service": "radicle-http-api",
"version": "0.2.0",
"path": "/",
"links": [
{
"href": "/v1/projects",
"rel": "projects",
"type": "GET"
},
{
"href": "/v1/peer",
"rel": "peer",
"type": "GET"
},
{
"href": "/v1/delegates/:urn/projects",
"rel": "projects",
"type": "GET"
}
]
}
-16
View File
@@ -1,16 +0,0 @@
{
"urn": "rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy",
"name": "bright-forest-protocol",
"description": "bfc-sc",
"defaultBranch": "main",
"delegates": [
{
"type": "indirect",
"urn": "rad:git:hnrkqz68g6nddigodpgjmc1u8ydpzb8fqq8ro",
"ids": ["hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke"]
}
],
"head": "56e4e029c294b08546386e1fb706b772c7433c49",
"patches": 2,
"issues": 3
}
-62
View File
@@ -1,62 +0,0 @@
[
{
"id": "hnrk8qmheqbwxius3brqxohqrfmgdrphtirxo",
"author": {
"peer": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"urn": "rad:git:hnrk81wcokr48mkm544kh74kc9fqz84d3rfcy",
"profile": {
"name": "sebastinez"
}
},
"title": "Testing strategy",
"state": {
"status": "open"
},
"comment": {
"author": {
"peer": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"urn": "rad:git:hnrk81wcokr48mkm544kh74kc9fqz84d3rfcy",
"profile": {
"name": "sebastinez"
}
},
"body": "We should define the scope we want to test in integration vs component testing (I don't mention E2E here on purpose since in our E2E tests we stubbed all responses and never hit a real server which equals to integration testing).\n\nWhile integration testing allows us to traverse the application and test navigation, component testing allows us to mount a component in isolation and check all the possible edge cases and possible regressions, and unit testing goes one step further to just individual functions on their correctness.\n\nIntegration testing npm run test:e2e eventually should be npm run test:integration\n\n**Connection through mocked Web3Provider**\n- Navigation through different URLs (e.g. the project pages, commits, issues, patches, etc.)\n- Assert the URLs we're hitting and that they update correctly, when state changes\n- Assert the least amount of display logic (we should leave that to component testing)\n If we are able to navigate to the correct URL and get the correct props, we shouldn't need to assert every piece of text.\nComponent testing npm run test:components\n\n- Assert the correct rendering\n- Test that the state of the component updates accordingly where applicable.\n**Unit tests npm run test:unit**\n\n- Util functions\nClasses (static and methods)\nI'll keep updating this issue.\nP.S.: Testing is not my expertise so any feedback is welcome.",
"reactions": {},
"replies": null,
"timestamp": 1656509367
},
"discussion": [],
"labels": ["discussion"],
"timestamp": 1656509367
},
{
"id": "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio",
"author": {
"peer": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"urn": "rad:git:hnrk81wcokr48mkm544kh74kc9fqz84d3rfcy",
"profile": {
"name": "sebastinez"
}
},
"title": "Update README",
"state": {
"status": "closed"
},
"comment": {
"author": {
"peer": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"urn": "rad:git:hnrk81wcokr48mkm544kh74kc9fqz84d3rfcy",
"profile": {
"name": "sebastinez"
}
},
"body": "We should define the scope we want to test in integration vs component testing (I don't mention E2E here on purpose since in our E2E tests we stubbed all responses and never hit a real server which equals to integration testing).\n\nWhile integration testing allows us to traverse the application and test navigation, component testing allows us to mount a component in isolation and check all the possible edge cases and possible regressions, and unit testing goes one step further to just individual functions on their correctness.\n\nIntegration testing npm run test:e2e eventually should be npm run test:integration\n\n**Connection through mocked Web3Provider**\n- Navigation through different URLs (e.g. the project pages, commits, issues, patches, etc.)\n- Assert the URLs we're hitting and that they update correctly, when state changes\n- Assert the least amount of display logic (we should leave that to component testing)\n If we are able to navigate to the correct URL and get the correct props, we shouldn't need to assert every piece of text.\nComponent testing npm run test:components\n\n- Assert the correct rendering\n- Test that the state of the component updates accordingly where applicable.\n**Unit tests npm run test:unit**\n\n- Util functions\nClasses (static and methods)\nI'll keep updating this issue.\nP.S.: Testing is not my expertise so any feedback is welcome.",
"reactions": {},
"replies": null,
"timestamp": 1656509367
},
"discussion": [],
"labels": ["discussion"],
"timestamp": 1656509367
}
]
-16
View File
@@ -1,16 +0,0 @@
[
{
"urn": "rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy",
"name": "mocked-seed-protocol",
"description": "bfc-sc",
"defaultBranch": "main",
"delegates": [
{
"type": "indirect",
"urn": "rad:git:hnrkqz68g6nddigodpgjmc1u8ydpzb8fqq8ro",
"ids": ["hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke"]
}
],
"head": "56e4e029c294b08546386e1fb706b772c7433c49"
}
]
-88
View File
@@ -1,88 +0,0 @@
[
{
"id": "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio",
"author": {
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"urn": "rad:git:hnrkkxqo5jgx3bwbphdsmqyk9djkfr368czio",
"profile": {
"name": "Scooby",
"ens": null
}
},
"title": "this is a patch with multiple commits and a merge base",
"state": "proposed",
"target": "upstream",
"labels": [],
"revisions": [
{
"id": "d2a46e1f-88a9-4e72-bc68-3f6afc92003c",
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"base": "58c1864cb6c6edd8682830d087bdcef7902cf62a",
"oid": "a36d8df141a0d3cedf47c4a42383ed98818195de",
"comment": {
"author": {
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"urn": "rad:git:hnrkkxqo5jgx3bwbphdsmqyk9djkfr368czio",
"profile": {
"name": "Scooby",
"ens": null
}
},
"body": "Signed-off-by: Sebastian Martinez <me@sebastinez.dev>",
"reactions": {},
"replies": null,
"timestamp": 1656666433
},
"discussion": [],
"reviews": {},
"merges": [],
"changeset": null,
"timestamp": 1656666433
}
],
"timestamp": 1656666433
},
{
"id": "hnrkqjf6ucd3o8ffztt7yyeatuj9u4k4957xo",
"author": {
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"urn": "rad:git:hnrkkxqo5jgx3bwbphdsmqyk9djkfr368czio",
"profile": {
"name": "Scooby",
"ens": null
}
},
"title": "Removing files",
"state": "proposed",
"target": "upstream",
"labels": [],
"revisions": [
{
"id": "7edbb9df-e921-4dff-b2e2-5b0a50c51188",
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"base": "58c1864cb6c6edd8682830d087bdcef7902cf62a",
"oid": "6da5af5cf09b5db638b8a6f5ce386391c3b74da4",
"comment": {
"author": {
"peer": "hybepksbf5xzew3ztear7k7peddho5wfts3d8uyb9pcdu6sqgnwqak",
"urn": "rad:git:hnrkkxqo5jgx3bwbphdsmqyk9djkfr368czio",
"profile": {
"name": "Scooby",
"ens": null
}
},
"body": "Signed-off-by: Sebastian Martinez <me@sebastinez.dev>",
"reactions": {},
"replies": null,
"timestamp": 1656667614
},
"discussion": [],
"reviews": {},
"merges": [],
"changeset": null,
"timestamp": 1656667614
}
],
"timestamp": 1656667614
}
]
-1
View File
@@ -1 +0,0 @@
{ "id": "hyy841u4phudmr8s5rg1jjwd1ct7x7438wmjwtsm464y8uyxyhyi6c" }
-24
View File
@@ -1,24 +0,0 @@
{
"binary": false,
"html": false,
"content": "# Basic Sample Hardhat Project\n\nThis project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, a sample script that deploys that contract, and an example of a task implementation, which simply lists the available accounts.\n\nTry running some of the following tasks:\n\n```shell\nnpx hardhat accounts\nnpx hardhat compile\nnpx hardhat clean\nnpx hardhat test\nnpx hardhat node\nnode scripts/sample-script.js\nnpx hardhat help\n```\n",
"info": {
"name": "README.md",
"objectType": "BLOB",
"lastCommit": {
"sha1": "cbf5df499ab4f4a908f1756fbe2c236a4530516a",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "initial commit",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646236685
}
},
"path": "README.md"
}
-7
View File
@@ -1,7 +0,0 @@
[
{
"id": "hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke",
"person": { "name": "dabit3" },
"delegate": true
}
]
-100
View File
@@ -1,100 +0,0 @@
{
"path": "",
"entries": [
{
"path": "contracts",
"info": {
"name": "contracts",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": "scripts",
"info": {
"name": "scripts",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": "test",
"info": {
"name": "test",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": ".gitignore",
"info": {
"name": ".gitignore",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "README.md",
"info": {
"name": "README.md",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "hardhat.config.js",
"info": {
"name": "hardhat.config.js",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "package-lock.json",
"info": {
"name": "package-lock.json",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "package.json",
"info": {
"name": "package.json",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "yarn.lock",
"info": {
"name": "yarn.lock",
"objectType": "BLOB",
"lastCommit": null
}
}
],
"info": {
"name": "",
"objectType": "TREE",
"lastCommit": {
"sha1": "56e4e029c294b08546386e1fb706b772c7433c49",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "emit event",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646237176
}
},
"stats": {
"commits": 3,
"branches": 1,
"contributors": 1
}
}
-100
View File
@@ -1,100 +0,0 @@
{
"path": "",
"entries": [
{
"path": "contracts",
"info": {
"name": "contracts",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": "scripts",
"info": {
"name": "scripts",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": "test",
"info": {
"name": "test",
"objectType": "TREE",
"lastCommit": null
}
},
{
"path": ".gitignore",
"info": {
"name": ".gitignore",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "README.md",
"info": {
"name": "README.md",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "hardhat.config.js",
"info": {
"name": "hardhat.config.js",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "package-lock.json",
"info": {
"name": "package-lock.json",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "package.json",
"info": {
"name": "package.json",
"objectType": "BLOB",
"lastCommit": null
}
},
{
"path": "yarn.lock",
"info": {
"name": "yarn.lock",
"objectType": "BLOB",
"lastCommit": null
}
}
],
"info": {
"name": "",
"objectType": "TREE",
"lastCommit": {
"sha1": "56e4e029c294b08546386e1fb706b772c7433c49",
"author": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"summary": "emit event",
"description": "",
"committer": {
"name": "dabit3",
"email": "dabit3@gmail.com"
},
"committerTime": 1646237176
}
},
"stats": {
"commits": 3,
"branches": 1,
"contributors": 1
}
}
-16
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/typography.css" />
<link rel="stylesheet" type="text/css" href="/colors.css" />
<link rel="stylesheet" type="text/css" href="/elevations.css" />
<link rel="stylesheet" type="text/css" href="/layout.css" />
<link rel="stylesheet" type="text/css" href="/index.css" />
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
-16
View File
@@ -1,16 +0,0 @@
import { mount } from "cypress/svelte";
import { Buffer } from "buffer";
//@ts-expect-error We need Buffer on the window object in the test env for component testing
window.Buffer = Buffer;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Cypress.Commands.add("mount", mount);
-13
View File
@@ -1,13 +0,0 @@
declare global {
interface Window {
ethereum: any;
localStorage: Storage;
}
}
function getPath(location: Location): string {
const url = location.href.replace(window.origin, "");
return process.env.hashRouting ? url.substring(2) : url;
}
export { getPath };
+253 -2327
View File
File diff suppressed because it is too large Load Diff
+13 -10
View File
@@ -10,28 +10,33 @@
"check": "scripts/check",
"format": "npx prettier '**/*.@(ts|js|svelte|json|css|html)' --ignore-path .gitignore --write",
"test:unit": "TZ='UTC' vitest run",
"test:components": "cypress run --component",
"test:e2e": "cypress run",
"test:open": "cypress open"
"test:e2e": "TZ='UTC' playwright test"
},
"type": "module",
"engines": {
"node": ">=18.12.1"
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@rsksmart/mock-web3-provider": "^1.0.1",
"@sveltejs/vite-plugin-svelte": "^1.3.0",
"@sinonjs/fake-timers": "^10.0.0",
"@sveltejs/vite-plugin-svelte": "^1.3.1",
"@tsconfig/svelte": "^3.0.0",
"@types/dompurify": "^2.4.0",
"@types/katex": "^0.14.0",
"@types/lodash": "^4.14.189",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"cypress": "^10.11.0",
"@types/lodash": "^4.14.190",
"@types/marked": "^4.0.7",
"@types/md5": "^2.3.2",
"@types/node": "^18.11.9",
"@types/sinonjs__fake-timers": "^8.1.2",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"chalk": "^5.1.2",
"eslint": "^8.28.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte-check": "^2.9.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vite-plugin-rewrite-all": "^1.0.0",
@@ -41,8 +46,6 @@
"@ethersproject/abstract-provider": "^5.4.0",
"@radicle/gray-matter": "4.1.0",
"@stardazed/streams": "^3.1.0",
"@types/marked": "^4.0.7",
"@types/md5": "^2.3.2",
"@walletconnect/client": "^1.8.0",
"buffer": "^6.0.3",
"dompurify": "^2.4.1",
@@ -51,7 +54,7 @@
"katex": "^0.16.3",
"lodash": "^4.17.21",
"lru-cache": "^7.14.1",
"marked": "^4.2.2",
"marked": "^4.2.3",
"md5": "^2.3.0",
"plausible-tracker": "^0.3.8",
"pure-svg-code": "^1.0.6",
+19
View File
@@ -0,0 +1,19 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import base from "./playwright.config.js";
const config: PlaywrightTestConfig = {
...base,
use: {
...base.use,
baseURL: "http://localhost:4173",
},
retries: 0,
globalSetup: undefined,
webServer: {
command: "npm run serve",
port: 4173,
},
};
export default config;
+50
View File
@@ -0,0 +1,50 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = {
testDir: "./tests/e2e",
timeout: 30_000,
expect: {
timeout: 8000,
},
fullyParallel: true,
outputDir: "./tests/artifacts",
workers: process.env.CI ? 1 : undefined,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: "list",
globalSetup: "./tests/support/globalSetup",
use: {
actionTimeout: 0,
baseURL: "http://localhost:3000",
trace: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
],
webServer: {
command: "npm run start",
port: 3000,
},
};
export default config;
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
function cleanup {
docker kill radicle-git-server-test
}
trap cleanup EXIT
PASSPHRASE=asdf
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
REPO_ROOT=$(git rev-parse --show-toplevel)
ID=$(echo $RANDOM | md5sum | head -c 8)
BASE_PATH=$REPO_ROOT/tests/tmp/create-seed-fixture-$ID
TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2
TEST_REPO_NAME=source-browsing
TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME
PALM_RAD_HOME=$BASE_PATH/seeds/palm
ALICE_RAD_HOME=$BASE_PATH/peers/alice
ALICE_CHECKOUT=$BASE_PATH/checkout/alice
BOB_RAD_HOME=$BASE_PATH/peers/bob
BOB_CHECKOUT=$BASE_PATH/checkout/bob
mkdir -p $PALM_RAD_HOME
mkdir -p $ALICE_RAD_HOME
mkdir -p $ALICE_CHECKOUT
mkdir -p $BOB_RAD_HOME
mkdir -p $BOB_CHECKOUT
mkdir -p $TEST_REPO_PATH
tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH
RAD_HOME=$PALM_RAD_HOME rad auth --init --name palm --passphrase $PASSPHRASE
docker run \
--detach \
--init \
--publish 8778:8778 \
--rm \
--env RAD_HOME=/app/radicle \
--name radicle-git-server-test \
--volume $PALM_RAD_HOME:/app/radicle \
"gcr.io/radicle-services/git-server:$REV" \
--passphrase $PASSPHRASE \
--allow-unauthorized-keys
# git-server takes a while to copy commit hooks to the monorepo
sleep 10
GIT_AUTHOR_NAME="Alice Liddell"
GIT_AUTHOR_EMAIL="alice@radicle.xyz"
GIT_COMMITTER_NAME="Alice Liddell"
GIT_COMMITTER_EMAIL="alice@radicle.xyz"
RAD_HOME=$ALICE_RAD_HOME rad auth --init --name alice --passphrase $PASSPHRASE
cd $ALICE_CHECKOUT
git clone $TEST_REPO_PATH
cd $TEST_REPO_NAME
git checkout feature/branch
git checkout orphaned-branch
git checkout main
RAD_HOME=$ALICE_RAD_HOME rad init --name "source-browsing" --description "Git repository for source browsing tests" --default-branch "main" --no-confirm
RAD_HOME=$ALICE_RAD_HOME rad push --seed 0.0.0.0:8778 --all --sync
PROJECT_URN=$(rad .)
GIT_AUTHOR_NAME="Bob Belcher"
GIT_AUTHOR_EMAIL="bob@radicle.xyz"
GIT_COMMITTER_NAME="Bob Belcher"
GIT_COMMITTER_EMAIL="bob@radicle.xyz"
RAD_HOME=$BOB_RAD_HOME rad auth --init --name bob --passphrase $PASSPHRASE
cd $BOB_CHECKOUT
RAD_HOME=$BOB_RAD_HOME rad clone $PROJECT_URN --seed 0.0.0.0:8778 --no-confirm
cd $TEST_REPO_NAME
echo "Updated readme" > README.md
git add README.md
git commit --message "Update readme" --date "Mon Nov 21 14:00 2022 +0100"
RAD_HOME=$BOB_RAD_HOME rad push --seed 0.0.0.0:8778
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778 --self
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778
cd $BASE_PATH
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME .
+132
View File
@@ -0,0 +1,132 @@
#!/bin/sh
set -e
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
REPO_ROOT=$(git rev-parse --show-toplevel)
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
WORKSPACE=$REPO_ROOT/tests/tmp/palm
PASSPHRASE=asdf
CONTAINER_NAME=radicle-http-api-with-fixtures
HTTP_API_BINARY=radicle-http-api
show_usage()
{
echo
echo "Starts a http-api backend with test fixtures."
echo
echo "USAGE:"
echo " run-http-api-with-fixtures [-b|d|h|n]"
echo
echo "OPTIONS:"
echo " -b --binary Use a ${HTTP_API_BINARY} binary that is in PATH to avoid using Docker."
echo " -d --detach Daemonize the docker process."
echo " -h --help Print this Help."
echo " -n --non-interactive Run in non-interactive mode, no user prompts."
echo
}
prompt_workspace_removal()
{
echo "This will irrevocably destroy the following directories:"
echo
echo $WORKSPACE
echo
read -r -p "Are you sure you want to continue? [yes/no]: " confirm
case "$confirm" in
[yY][eE][sS] )
rm -rf $WORKSPACE
echo "Done"
;;
* )
echo "Ok, I won't touch your data."
exit
;;
esac
}
prepare_workspace()
{
echo
echo "Unpacking fixture $FIXTURE"
mkdir -p $WORKSPACE
tar -xf $FIXTURE -C $WORKSPACE
}
run_docker() {
echo "Starting docker at container $CONTAINER_NAME at $REV"
echo " http-api --root $WORKSPACE --passphrase $PASSPHRASE"
echo
exec docker run \
--init \
--publish 8777:8777 \
--rm \
--name $CONTAINER_NAME \
--volume $WORKSPACE:/app/radicle \
"$@" \
"gcr.io/radicle-services/http-api:$REV" \
--passphrase $PASSPHRASE
}
run_binary() {
if ! [ -x "$(command -v $HTTP_API_BINARY)" ]; then
echo
echo "Couldn't find the $HTTP_API_BINARY binary in your PATH."
echo "You can compile it from source:"
echo " 👉 https://github.com/radicle-dev/radicle-client-services/tree/${REV}/http-api"
echo
exit 1
fi
echo
echo "Starting $HTTP_API_BINARY"
echo " $HTTP_API_BINARY --listen 0.0.0.0:8777 --root ${WORKSPACE} --passphrase $PASSPHRASE"
echo
$HTTP_API_BINARY --listen 0.0.0.0:8777 --root $WORKSPACE --passphrase $PASSPHRASE
}
BINADY=false
NON_INTERACTIVE=false
DETACH=false
while [ $# -ne 0 ]; do
case "$1" in
--binary|-b)
BINARY=true
;;
--detach|-d)
DETACH=true
;;
--non-interactive|-n)
NON_INTERACTIVE=true
;;
*)
show_usage
exit
;;
esac
shift
done
if [ "$NON_INTERACTIVE" = true ]; then
rm -rf $WORKSPACE
prepare_workspace
else
prompt_workspace_removal
prepare_workspace
fi
if [ "$BINARY" = true ]; then
run_binary
else
if [ "$DETACH" = true ]; then
run_docker --detach
else
run_docker
fi
fi
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
set -e
npm run test:unit
npm run test:components
+7 -5
View File
@@ -20,12 +20,14 @@
initialize();
const plausible = Plausible({
domain: "app.radicle.xyz",
hashMode: Boolean(process.env.hashRouting),
});
if (!window.VITEST && !window.PLAYWRIGHT && import.meta.env.PROD) {
const plausible = Plausible({
domain: "app.radicle.xyz",
hashMode: window.HASH_ROUTING,
});
plausible.enableAutoPageviews();
plausible.enableAutoPageviews();
}
const loadWallet = getWallet().then(async wallet => {
if ($state.connection === Connection.Connected) {
-71
View File
@@ -1,71 +0,0 @@
import BlockTimer from "./BlockTimer.svelte";
describe("BlockTimer", () => {
describe("when latestBlock === startBlock", () => {
it("shows 0% progress", () => {
cy.mount(BlockTimer, {
props: {
latestBlock: 1,
startBlock: 1,
duration: 3,
},
});
cy.get(".progress-bar").should("have.attr", "style", "width: 0%;");
});
});
describe("when latestBlock < duration + startBlock", () => {
it("shows 33% progress", () => {
cy.mount(BlockTimer, {
props: {
latestBlock: 2,
startBlock: 1,
duration: 3,
},
});
cy.get(".progress-bar").should("have.attr", "style", "width: 33%;");
});
it("shows 66% progress", () => {
cy.mount(BlockTimer, {
props: {
latestBlock: 3,
startBlock: 1,
duration: 3,
},
});
cy.get(".progress-bar").should("have.attr", "style", "width: 66%;");
});
});
describe("when latestBlock === duration + startBlock", () => {
it("shows 100% progress", () => {
cy.mount(BlockTimer, {
props: {
latestBlock: 4,
startBlock: 1,
duration: 3,
},
});
cy.get(".progress-bar").should("have.attr", "style", "width: 100%;");
});
});
describe("when latestBlock > duration + startBlock", () => {
it("shows 100% progress", () => {
cy.mount(BlockTimer, {
props: {
latestBlock: 6,
startBlock: 1,
duration: 3,
},
});
cy.get(".progress-bar").should("have.attr", "style", "width: 100%;");
});
});
});
-44
View File
@@ -1,44 +0,0 @@
import ErrorModal from "./ErrorModal.svelte";
import { Failure } from "@app/error";
describe("Error", () => {
it("should show passed in props", () => {
cy.mount(ErrorModal, {
props: {
subtitle: "Subtitle of Modal",
error: {
type: Failure.InsufficientBalance,
txHash:
"0x8b678e51f970c5307bf45a8bcea373b597f9acbcea5c5ba784a1d383361a89d1",
message: "Not enough RAD",
},
},
});
cy.get("body").contains("Error").should("be.visible");
cy.get("body").contains("Subtitle of Modal").should("be.visible");
cy.get("body").contains("Not enough RAD").should("be.visible");
cy.get("button").contains("Back").should("be.visible");
});
it("should show custom error message", () => {
cy.mount(ErrorModal, {
props: {
subtitle: "Subtitle of Modal",
message: "Error message to check for",
},
});
cy.get("body").contains("Error message to check for").should("be.visible");
});
it("should change button label to Close when floating", () => {
cy.mount(ErrorModal, {
props: {
title: "Title of Modal",
subtitle: "Subtitle of Modal",
message: "Error message to check for",
floating: true,
},
});
cy.get("button").contains("Close").should("be.visible");
});
});
+6 -2
View File
@@ -143,6 +143,8 @@
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
color: var(--color-foreground);
}
.toggle:hover {
background-color: var(--color-foreground);
@@ -233,8 +235,10 @@
</span>
{/if}
<Floating>
<div class="toggle" slot="toggle">
<Icon name="gear" />
<div slot="toggle">
<button class="toggle" name="Settings">
<Icon name="gear" />
</button>
</div>
<SettingsDropdown slot="modal" />
</Floating>
-17
View File
@@ -1,17 +0,0 @@
import NotFound from "./NotFound.svelte";
describe("NotFound", () => {
it("shows passed props correctly", () => {
cy.mount(NotFound, {
props: {
title: "nakamoto",
subtitle: "Sorry, the requested project was not found.",
},
});
cy.get("body").contains("nakamoto").should("be.visible");
cy.get("body")
.contains("Sorry, the requested project was not found.")
.should("be.visible");
cy.get("button").contains("Back").should("be.visible");
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
<script lang="ts" strictEvents>
import { createEventDispatcher } from "svelte";
import Icon from "@app/Icon.svelte";
import config from "@app/config.json";
import { config } from "@app/config";
const showReactions = false;
+5 -3
View File
@@ -7,7 +7,7 @@
import * as utils from "@app/utils";
import { Profile } from "@app/profile";
import { Project } from "@app/project";
import config from "@app/config.json";
import { config } from "@app/config";
export interface ProjectsAndProfiles {
projects: { info: ProjectInfo; seed: Host }[];
@@ -46,6 +46,8 @@
seedHost: projects[0].seed.host,
id: query,
};
} else if (projects.length === 0) {
return { type: "nothing" };
} else {
return {
type: "projectsAndProfiles",
@@ -203,7 +205,7 @@
</script>
<style>
.search {
.search-bar {
display: flex;
}
.shaking {
@@ -228,7 +230,7 @@
}
</style>
<div class="search" class:shaking>
<div class="search-bar" class:shaking>
<TextInput
variant="dashed"
valid={input !== ""}
-50
View File
@@ -1,50 +0,0 @@
import { Seed } from "@app/base/seeds/Seed";
import SeedAddress from "./SeedAddress.svelte";
describe("SeedAddress", () => {
it("shows the seed emoji and seed host", () => {
const seed = new Seed({
host: "seed.cloudhead.io",
id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
});
cy.mount(SeedAddress, {
props: {
seed,
port: 8776,
},
});
cy.get("span.seed-icon img").should("have.attr", "alt", "🌱");
cy.get("span.seed-icon img").should(
"have.attr",
"src",
"/twemoji/1f331.svg",
);
cy.contains("seed.cloudhead.io").should("be.visible");
});
it("shows the full seed id", () => {
const seed = new Seed({
host: "seed.cloudhead.io",
id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
});
cy.mount(SeedAddress, {
props: {
seed,
port: 8776,
full: true,
},
});
cy.get("span.seed-icon img").should("have.attr", "alt", "🌱");
cy.get("span.seed-icon img").should(
"have.attr",
"src",
"/twemoji/1f331.svg",
);
cy.get("body")
.contains("hydkkk…coygh1@seed.cloudhead.io")
.should("be.visible");
cy.get("body").contains(":8776").should("be.visible");
});
});
+1 -1
View File
@@ -6,7 +6,7 @@
import Loading from "@app/Loading.svelte";
import Message from "@app/Message.svelte";
import Widget from "@app/base/projects/Widget.svelte";
import config from "@app/config.json";
import { config } from "@app/config";
import { Project } from "@app/project";
import { setOpenGraphMetaTag, twemoji } from "@app/utils";
-183
View File
@@ -1,183 +0,0 @@
import type { ProjectInfo } from "@app/project";
import BranchSelector from "./BranchSelector.svelte";
const project: ProjectInfo = {
head: "e678629cd37c770c640a2cd997fc76303c815772",
urn: "rad:git:hnrkqdpm9ub19oc8dccx44echy76hzfsezyio",
name: "nakamoto",
description: "Privacy-preserving Bitcoin light-client implementation in Rust",
defaultBranch: "master",
remotes: ["rad:git:hnrkqdpm9ub19oc8dccx44echy76hzfsezyio"],
delegates: [
{
type: "direct",
id: "hyn9diwfnytahjq8u3iw63h9jte1ydcatxax3saymwdxqu1zo645pe",
},
],
};
const defaultProps = {
project,
branches: { master: "e678629cd37c770c640a2cd997fc76303c815772" },
revision: "e678629cd37c770c640a2cd997fc76303c815772",
};
describe("Logic", () => {
it("should show defaultBranch label and head commit if revision === head", () => {
cy.mount(BranchSelector, {
props: defaultProps,
});
cy.get("div.stat.branch")
.should("be.visible")
.should("have.text", "master");
cy.get("div.hash.layout-mobile")
.should("be.visible")
.should("have.text", "e678629");
});
it("if project.head is null we should get the head from branches", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
project: {
...project,
head: null,
},
},
});
cy.get("div.stat.branch")
.should("be.visible")
.should("have.text", "master");
cy.get("div.hash.layout-mobile")
.should("be.visible")
.should("have.text", "e678629");
});
it("should show the branch dropdown if branches available", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
branches: {
master: "e678629cd37c770c640a2cd997fc76303c815772",
"feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
xyz: "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
},
});
cy.get("div.commit div.stat.branch").click();
cy.get("div.dropdown div.dropdown-item")
.first()
.should("contain.text", "feature-branch")
.next()
.should("contain.text", "master")
.should("have.class", "selected")
.next()
.should("contain.text", "xyz");
});
it("should show feature-branch label and head commit, if branch label is passed as revision", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
branches: {
master: "e678629cd37c770c640a2cd997fc76303c815772",
"feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
xyz: "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
revision: "feature-branch",
},
});
cy.get("div.stat.branch")
.should("be.visible")
.should("have.text", "feature-branch");
cy.get("div.hash.layout-mobile")
.should("be.visible")
.should("have.text", "29e8b7b");
});
it("should show only commit if no branchLabel nor branches are available", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
revision: "debf82ef3623ec11751a993bda85bac2ff1c6f00",
branches: {},
},
});
cy.get("div.hash.layout-mobile")
.should("be.visible")
.should("have.text", "debf82e");
cy.viewport("macbook-13");
cy.get("div.hash.layout-desktop")
.should("be.visible")
.should("have.text", "debf82ef3623ec11751a993bda85bac2ff1c6f00");
});
it("should show only commit if branches are available but no branchLabel", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
revision: "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
});
cy.get("div.hash.layout-mobile")
.should("be.visible")
.should("have.text", "debf82e");
cy.viewport("macbook-13");
cy.get("div.hash.layout-desktop")
.should("be.visible")
.should("have.text", "debf82ef3623ec11751a993bda85bac2ff1c6f00");
});
it("should show defaultBranch label if revision === head", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
revision: "e678629cd37c770c640a2cd997fc76303c815772",
branches: {},
},
});
cy.get("div.stat.branch.not-allowed")
.should("be.visible")
.should("have.text", "master");
});
});
describe("Layout", () => {
it("should show shortened commit when on mobile, and full hash when on desktop", () => {
cy.mount(BranchSelector, {
props: {
...defaultProps,
revision: "e678629cd37c770c640a2cd997fc76303c815772",
},
});
cy.viewport("iphone-x");
cy.get("div.hash.layout-mobile").should("be.visible");
cy.get("div.hash.layout-desktop").should("not.be.visible");
cy.viewport("macbook-15");
cy.get("div.hash.layout-mobile").should("not.be.visible");
cy.get("div.hash.layout-desktop").should("be.visible");
});
});
describe("Events", () => {
it("should dispatch a 'branchChanged' event on click", () => {
const branchChangedSpy = cy.spy().as("branchChangedSpy");
cy.mount(BranchSelector, {
props: {
...defaultProps,
revision: "feature-branch",
branches: {
"feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
xyz: "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
},
}).then(({ component }) => {
component.$on("branchChanged", branchChangedSpy);
});
cy.get("body").contains("feature-branch").click();
cy.get("body").contains("xyz").click();
cy.get("@branchChangedSpy").should("have.been.called");
});
});
+2 -1
View File
@@ -72,13 +72,14 @@
}
</style>
<div class="commit" title="Switch branches">
<div class="commit" title="Current branch">
<!-- Check for branches listing feature -->
{#if branchList.length > 0}
{#if branchLabel}
<Floating disabled={!showSelector}>
<div
slot="toggle"
title="Change branch"
class="stat branch"
class:not-allowed={!showSelector}>
{branchLabel}
+13 -5
View File
@@ -35,7 +35,10 @@
// Whether the mobile file tree is visible.
let mobileFileTree = false;
const loadBlob = async (path: string, theme: Theme): Promise<proj.Blob> => {
const loadBlob = async (
path: string,
theme: Theme,
): Promise<proj.Blob | undefined> => {
if (
state.status === Status.Loaded &&
state.path === path &&
@@ -57,9 +60,12 @@
);
state = { status: Status.Loading, path };
state = { status: Status.Loaded, path, blob: await promise, theme };
return state.blob;
try {
state = { status: Status.Loaded, path, blob: await promise, theme };
return state.blob;
} catch (err) {
console.warn("Could not load blob.");
}
};
// Get an image blob based on a relative path.
@@ -206,7 +212,9 @@
{#await getBlob}
<Loading small center />
{:then blob}
<Blob {line} {blob} {getImage} {activeRoute} />
{#if blob}
<Blob {line} {blob} {getImage} {activeRoute} />
{/if}
{:catch}
<Placeholder emoji="🍂">
<span slot="title">
-93
View File
@@ -1,93 +0,0 @@
import PeerSelector from "./PeerSelector.svelte";
const defaultProps = {
peer: "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
peers: [
{
id: "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
person: { name: "sebastinez" },
delegate: true,
},
],
};
describe("Logic", () => {
it("show delegate name and badge", () => {
cy.mount(PeerSelector, {
props: defaultProps,
});
cy.get("span.peer-id").should("have.text", "sebastinez");
cy.get("span.badge.primary").should("have.text", "delegate");
});
it("show peer id with badge if no name available", () => {
cy.mount(PeerSelector, {
props: {
...defaultProps,
peers: [
{
id: "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
delegate: true,
},
],
},
});
cy.get("span.peer-id").should("have.text", "hyyg55…p7ofue");
cy.get("span.badge.primary").should("have.text", "delegate");
});
it("show only peer id if no additional data available", () => {
cy.mount(PeerSelector, {
props: {
...defaultProps,
peers: [
{
id: "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
delegate: false,
},
],
},
});
cy.get("span.peer-id").should("have.text", "hyyg55…p7ofue");
});
});
describe("Layout", () => {
it("should highlight the current peer", () => {
cy.mount(PeerSelector, {
props: { ...defaultProps },
});
cy.get("div.selector").click();
cy.get("div.dropdown-item").should("have.class", "selected");
});
});
describe("Events", () => {
it("dispatch peerChanged event if clicking on a peer", () => {
const peerChangedSpy = cy.spy().as("peerChangedSpy");
cy.mount(PeerSelector, {
props: {
...defaultProps,
peers: [
{
id: "hyy841u4phudmr8s5rg1jjwd1ct7x7438wmjwtsm464y8uyxyhyi6c",
person: { name: "cloudhead" },
delegate: true,
},
{
id: "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
person: { name: "sebastinez" },
delegate: true,
},
],
},
}).then(({ component }) => {
component.$on("peerChanged", peerChangedSpy);
});
cy.get("body").contains("sebastinez").click();
cy.get("body").contains("cloudhead").click();
cy.get("@peerChangedSpy").should("have.been.called");
});
});
+1 -1
View File
@@ -85,7 +85,7 @@
</style>
<Floating>
<div slot="toggle" class="selector" title="Switch peers">
<div slot="toggle" class="selector" title="Change peer">
<div class="stat peer" class:not-allowed={!peers}>
<Icon name="fork" />
{#if meta}
+10 -7
View File
@@ -48,13 +48,16 @@
activeRoute.params.route,
project.branches,
);
router.updateProjectRoute({
revision,
path,
line: activeRoute.params.line,
hash: activeRoute.params.hash,
route: undefined,
});
router.updateProjectRoute(
{
revision,
path,
line: activeRoute.params.line,
hash: activeRoute.params.hash,
route: undefined,
},
{ replace: true },
);
}
if (!activeRoute.params.revision) {
// We need a revision to fetch `getRoot`.
-4
View File
@@ -60,10 +60,6 @@ export const state = writable<Connection>({ connection: State.Connecting });
window.registrarState = state;
state.subscribe((s: Connection) => {
console.debug("register.state", s);
});
export async function getRegistration(
name: string,
wallet: Wallet,
+36
View File
@@ -0,0 +1,36 @@
import configJson from "@app/config.json";
export interface Config {
walletConnect: { bridge: string };
reactions: string[];
seeds: {
pinned: { host: string; emoji: string }[];
};
projects: {
pinned: {
name: string;
urn: string;
seed: string;
}[];
};
}
function getConfig(): Config {
if (window.VITEST) {
return {
walletConnect: { bridge: "" },
reactions: [],
seeds: {
pinned: [],
},
projects: { pinned: [] },
};
} else if (window.PLAYWRIGHT) {
return window.APP_CONFIG;
} else {
// In dev and production environments we use data from config.json.
return configJson;
}
}
export const config = getConfig();
+8
View File
@@ -0,0 +1,8 @@
import * as FakeTimers from "@sinonjs/fake-timers";
if (typeof window.initializeTestStubs === "function") {
window.e2eTestStubs = {
FakeTimers: FakeTimers,
};
window.initializeTestStubs();
}
+32
View File
@@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { Config } from "@app/config";
import type { FakeTimers } from "@sinonjs/fake-timers";
declare global {
interface Window {
// Defined in vite.config.ts and are available in all environments except
// production.
VITEST: boolean;
PLAYWRIGHT: boolean;
HASH_ROUTING: boolean;
// APP_CONFIG is set from within Playwright tests at runtime.
// To better understand how it works together, have a look at:
// tests/support/fixtures.ts
// src/config.ts
APP_CONFIG: Config;
// eslint-disable-next-line @typescript-eslint/ban-types
initializeTestStubs: Function;
e2eTestStubs: {
FakeTimers: FakeTimers;
};
// Used in
// src/session.ts
// src/wallet.ts
ethereum: any;
registrarState: any;
}
}
export {};
+1 -8
View File
@@ -1,15 +1,8 @@
if (window.PLAYWRIGHT) import("./e2eTestStubs");
import App from "./App.svelte";
const app = new App({
target: document.body,
});
declare global {
namespace NodeJS {
interface ProcessEnv {
hashRouting: number;
}
}
}
export default app;
+14 -7
View File
@@ -16,13 +16,15 @@ export const activeRouteStore: Readable<Route> = derived(
},
);
export const base = process.env.hashRouting ? "./" : "/";
export const base = window.HASH_ROUTING ? "./" : "/";
// Gets triggered when clicking on an anchor hash tag e.g. <a href="#header"/>
// Allows the jump to a anchor hash
window.addEventListener("hashchange", e => {
const url = new URL(e.newURL);
updateProjectRoute({ hash: url.hash.substring(1) });
const route = pathToRoute(e.newURL);
if (route?.resource === "projects" && route.params.hash) {
updateProjectRoute({ hash: route.params.hash });
}
});
// Replaces history on any user interaction with forward and backwards buttons
@@ -62,12 +64,17 @@ export function projectLinkHref(
export function updateProjectRoute(
projectRouteParams: Partial<ProjectsParams>,
opts: { replace: boolean } = { replace: false },
) {
const activeRoute = get(activeRouteStore);
if (activeRoute.resource === "projects") {
const updatedRoute = createProjectRoute(activeRoute, projectRouteParams);
push(updatedRoute);
if (opts.replace) {
replace(updatedRoute);
} else {
push(updatedRoute);
}
} else {
throw new Error(
"Don't use project specific navigation outside of project views",
@@ -82,7 +89,7 @@ export const push = (newRoute: Route): void => {
// one subsequent pop() anyway.
historyStore.set([...history, newRoute].slice(-10));
const path = process.env.hashRouting
const path = window.HASH_ROUTING
? "#" + routeToPath(newRoute)
: routeToPath(newRoute);
@@ -101,7 +108,7 @@ export const pop = (): void => {
export function replace(newRoute: Route): void {
historyStore.set([newRoute]);
const path = process.env.hashRouting
const path = window.HASH_ROUTING
? "#" + routeToPath(newRoute)
: routeToPath(newRoute);
@@ -127,7 +134,7 @@ function pathToRoute(path: string): Route | null {
}
const url = new URL(path, window.origin);
const segments = process.env.hashRouting
const segments = window.HASH_ROUTING
? url.hash.substring(2).split("#")[0].split("/") // Try to remove any additional hashes at the end of the URL.
: url.pathname.substring(1).split("/");
-4
View File
@@ -404,10 +404,6 @@ export async function connectSeed(seedSession: {
state.connectSeed(seedSession);
}
state.subscribe(s => {
console.debug("session.state", s);
});
export async function approveSpender(
spender: string,
amount: BigNumber,
+1 -1
View File
@@ -3,7 +3,6 @@ import type { Wallet } from "@app/wallet";
import type { marked } from "marked";
import * as cache from "@app/cache";
import config from "@app/config.json";
import emojis from "@app/emojis";
import katex from "katex";
import md5 from "md5";
@@ -12,6 +11,7 @@ import { BigNumber } from "ethers";
import { ProfileType } from "@app/profile";
import { assert } from "@app/error";
import { base } from "@app/router";
import { config } from "@app/config";
import { ethers } from "ethers";
import { getAddress, getResolver } from "@app/base/registrations/registrar";
import {
+10 -12
View File
@@ -10,7 +10,7 @@ import { capitalize } from "@app/utils";
import ethereumContractAbis from "@app/ethereum/contractAbis.json";
import homestead from "@app/ethereum/networks/homestead.json";
import goerli from "@app/ethereum/networks/goerli.json";
import config from "@app/config.json";
import { config } from "@app/config";
interface NetworkConfig {
name: string;
@@ -29,15 +29,6 @@ interface NetworkConfig {
alchemy: { key: string };
}
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
Cypress: any;
ethereum: any;
registrarState: any;
}
}
export type WalletConnectState =
| { state: "close" }
| { state: "open"; uri: string; onClose: any };
@@ -200,12 +191,19 @@ function getProvider(
): ethers.providers.JsonRpcProvider {
if (metamask) {
return metamask;
} else if (import.meta.env.PROD) {
} else if (
import.meta.env.PROD &&
window.location.host !== "localhost:4173"
) {
return new ethers.providers.AlchemyWebSocketProvider(
networkConfig.name,
networkConfig.alchemy.key,
);
} else if (import.meta.env.DEV) {
}
// Run the production smoke test with the ethers provider,
// because we block requests from localhost on Alchemy,
// which in turn throws an exception.
else if (import.meta.env.DEV || window.location.host === "localhost:4173") {
// The ethers defaultProvider doesn't include a `send` method, which breaks the `utils.getTokens` fn.
// Since Metamask nor WalletConnect provide an `alchemy_getTokenBalances` nor `alchemy_getTokenMetadata` endpoint,
// we can rely on not using `config.provider.send`.
+18
View File
@@ -0,0 +1,18 @@
import { test, expect } from "@tests/support/fixtures.js";
test("exceptions in production build", async ({ page, browserName }) => {
// It's enough to check this once.
if (browserName !== "chromium") {
test.skip();
}
await page.goto("/");
// Wait for scripts to finish executing, there might be exceptions that
// happen after the page has been painted.
await page.waitForTimeout(2000);
await expect(
page.locator(
"text=Radicle enables developers to securely collaborate on software over a peer-to-peer network built on Git.",
),
).toBeVisible();
});
+87
View File
@@ -0,0 +1,87 @@
import type { Page } from "@playwright/test";
import { test, expect } from "@tests/support/fixtures.js";
const sourceBrowsingFixture =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o";
async function expectClipboard(content: string, page: Page) {
const clipboardContent = await page.evaluate<string>(
"navigator.clipboard.readText()",
);
expect(clipboardContent).toBe(content);
}
// We explicitly run all clipboard tests withing the context of a single test
// so that we don't run into race conditions, because there is no way to isolate
// the clipboard in Playwright yet.
test("copy to clipboard", async ({ page, browserName, context }) => {
// These tests only work in Chromium, because other browsers don't support
// changing permissions.
if (browserName !== "chromium") {
test.skip();
}
context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto(sourceBrowsingFixture);
// Reset system clipboard to a known state.
await page.evaluate<string>("navigator.clipboard.writeText('')");
// Project URN.
{
await page.locator(".urn > .clipboard").click();
const clipboardContent = await page.evaluate<string>(
"navigator.clipboard.readText()",
);
expect(clipboardContent).toBe(
"rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
);
}
// `rad clone` URL.
{
await page.getByText("Clone").click();
await page.locator("text=rad clone rad://0.0.0.0/hnrkgd").hover();
await page
.locator(".clone-url-wrapper > span")
.first()
.locator(".clipboard")
.click();
await expectClipboard(
"rad clone rad://0.0.0.0/hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
page,
);
}
// `git clone` URL.
{
await page.getByText("Clone").click();
await page.locator("text=https://0.0.0.0/hnrkgd").hover();
await page
.locator(".clone-url-wrapper > span")
.last()
.locator(".clipboard")
.click();
await expectClipboard(
"https://0.0.0.0/hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o.git",
page,
);
}
await page.goto("/seeds/radicle.local");
// Seed address.
{
await page.locator(".clipboard").first().click();
await expectClipboard("0.0.0.0", page);
await page.locator(".clipboard").last().click();
await expectClipboard(
"hyb6i8oggc3mgra9siy8yuohhtz34r98pcybja97c9o789wpsg6nn4",
page,
);
}
// Clear the system clipboard contents so developers don't wonder why there's
// random stuff in their clipboard after running tests.
await page.evaluate<string>("navigator.clipboard.writeText('')");
});
+43
View File
@@ -0,0 +1,43 @@
import { test, expect } from "@tests/support/fixtures.js";
test.use({
customAppConfig: true,
});
test("show pinned projects", async ({ page }) => {
await page.addInitScript(() => {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
},
reactions: [],
seeds: {
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
},
projects: {
pinned: [
{
name: "source-browsing",
urn: "rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
seed: "0.0.0.0",
},
],
},
};
});
await page.goto("/");
await expect(
page.locator("text=Explore projects on the Radicle network."),
).toBeVisible();
// Shows pinned project name.
await expect(page.locator("text=source-browsing")).toBeVisible();
//
// Shows pinned project description.
await expect(
page.locator("text=Git repository for source browsing tests"),
).toBeVisible();
// Shows latest commit.
await expect(page.locator("text=530aabd")).toBeVisible();
});
+339
View File
@@ -0,0 +1,339 @@
import type { Page } from "@playwright/test";
import { test, expect } from "@tests/support/fixtures.js";
const sourceBrowsingFixture =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o";
async function expectCounts(
params: { commits: number; contributors: number },
page: Page,
) {
await expect(page.locator('role=button[name="Commit count"]')).toContainText(
`${params.commits} commit(s)`,
);
await expect(
page.locator('role=button[name="Contributor count"]'),
).toContainText(`${params.contributors} contributor(s)`);
}
test("navigate to project", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
// Header.
{
const name = page.locator("text=source-browsing");
const urn = page.locator(
"text=rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
);
const description = page.locator(
"text=Git repository for source browsing tests",
);
await expect(name).toBeVisible();
await expect(urn).toBeVisible();
await expect(description).toBeVisible();
}
// Project menu shows default selected branch and commit and contributor counts.
{
await expect(page.getByTitle("Current branch")).toContainText(
"main 530aabd",
);
await expectCounts({ commits: 7, contributors: 1 }, page);
}
// Navigate to the project README.md by default.
await expect(page.locator(".file-name")).toContainText("README.md");
// Show a commit teaser.
await expect(page.locator("text=dd068e9 Add README.md")).toBeVisible();
// Show rendered README.md contents.
await expect(page.locator("text=Git test repository")).toBeVisible();
});
test("show source tree at specific revision", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.locator('role=button[name="Commit count"]').click();
await page
.locator(".commit-teaser", { hasText: "335dd6d" })
.getByTitle("Browse the repository at this point in the history")
.click();
await expect(page.getByTitle("Current branch")).toContainText(
"335dd6dc89b535a4a31e9422c803199bb6b0a09a",
);
expect(page.locator(".source-tree")).toHaveText("bin/ src/");
await expectCounts({ commits: 2, contributors: 1 }, page);
});
test("source file highlighting", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByText("src/").click();
await page.getByText("true.c").click();
await expect(page.getByText("return")).toHaveCSS(
"color",
"rgb(180, 142, 173)",
);
});
test("navigate deep file hierarchies", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
const sourceTree = page.locator(".source-tree");
await sourceTree.getByText("deep/").click();
await sourceTree.getByText("directory/").click();
await sourceTree.getByText("hierarchy/").click();
await sourceTree.getByText("is/").click();
await sourceTree.getByText("entirely/").click();
await sourceTree.getByText("possible/").click();
await sourceTree.getByText("in/").nth(1).click();
await sourceTree.getByText("git/").click();
await sourceTree.getByText("repositories/").click();
await sourceTree.getByText(".gitkeep").click();
await expect(
page.locator("text=0801ace Add a deeply nested directory tree"),
).toBeVisible();
// After a page reload the tree browser is still expanded and we're still
// showing the .gitkeep file.
{
await page.reload();
const sourceTree = page.locator(".source-tree");
await expect(sourceTree.getByText("deep/")).toBeVisible();
await expect(sourceTree.getByText("directory/")).toBeVisible();
await expect(sourceTree.getByText("hierarchy/")).toBeVisible();
await expect(sourceTree.getByText("is/")).toBeVisible();
await expect(sourceTree.getByText("entirely/")).toBeVisible();
await expect(sourceTree.getByText("possible/")).toBeVisible();
await expect(sourceTree.getByText("in/").nth(1)).toBeVisible();
await expect(sourceTree.getByText("git/")).toBeVisible();
await expect(sourceTree.getByText("repositories/")).toBeVisible();
await expect(sourceTree.getByText(".gitkeep")).toBeVisible();
await expect(
page.locator("text=0801ace Add a deeply nested directory tree"),
).toBeVisible();
}
});
test("files with special characters in the filename", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
const sourceTree = page.locator(".source-tree");
await sourceTree.getByText("special/").click();
await sourceTree.getByText("+plus+").click();
await expect(page.locator(".file-name")).toContainText("+plus");
await sourceTree.getByText("-dash-").click();
await expect(page.locator(".file-name")).toContainText("-dash-");
await sourceTree.getByText(":colon:").click();
await expect(page.locator(".file-name")).toContainText(":colon:");
await sourceTree.getByText(";semicolon;").click();
await expect(page.locator(".file-name")).toContainText(";semicolon;");
await sourceTree.getByText("@at@").click();
await expect(page.locator(".file-name")).toContainText("@at@");
await sourceTree.getByText("_underscore_").click();
await expect(page.locator(".file-name")).toContainText("_underscore_");
// TODO: fix these errors in `racdicle-client-services/http-api` for the
// following edge cases.
//
// await sourceTree.getByText("back\\slash").click();
// await expect(page.locator(".file-name")).toContainText("back\\slash");
// await sourceTree.getByText("qs?param1=value?param2=value2#hash").click();
// await expect(page.locator(".file-name")).toContainText(
// "qs?param1=value?param2=value2#hash",
// );
await sourceTree.getByText("spaces are okay").click();
await expect(page.locator(".file-name")).toContainText("spaces are okay");
await sourceTree.getByText("~tilde~").click();
await expect(page.locator(".file-name")).toContainText("~tilde~");
await sourceTree.getByText("👹👹👹").click();
await expect(page.locator(".file-name")).toContainText("👹👹👹");
});
test("binary files", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByText("bin/").click();
await page.getByText("true").click();
await expect(page.locator("text=Binary content")).toBeVisible();
});
test("hidden files", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByText(".hidden").click();
await expect(page.locator("text=I'm a hidden file.")).toBeVisible();
});
test("markdown files", async ({ page }) => {
await page.goto(`${sourceBrowsingFixture}/tree/main/markdown/cheatsheet.md`);
await expect(
page.locator("text=This is intended as a quick reference and showcase."),
).toBeVisible();
// Switch between raw and rendered modes.
{
const rawButton = page.locator('role=button[name="Raw"]');
await rawButton.click();
await expect(rawButton).toHaveClass(/active/);
await expect(page.locator("text=##### Table of Contents")).toBeVisible();
await rawButton.click();
await expect(rawButton).not.toHaveClass("active");
}
// Internal links go to anchor.
{
await page.getByRole("link", { name: "YouTube Videos" }).click();
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/main/markdown/cheatsheet.md#videos",
);
}
});
test("peer and branch switching", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
// Alice's peer.
{
await page.getByTitle("Change peer").click();
await page.locator("text=alice").click();
await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
await expect(
page.locator("text=source-browsing / hyn1mj…qx7bun"),
).toBeVisible();
// Default `main` branch.
{
await expect(page.getByTitle("Current branch")).toContainText(
"main 530aabd",
);
await expectCounts({ commits: 7, contributors: 1 }, page);
}
// Feature branch with a slash in the name.
{
await page.getByTitle("Change branch").click();
await page.locator("text=feature/branch").click();
await expect(page.getByTitle("Current branch")).toContainText(
"feature/branch d6318f7",
);
await expectCounts({ commits: 10, contributors: 1 }, page);
}
// Branch without a history or files in it.
{
await page.getByTitle("Change branch").click();
await page.locator("text=orphaned-branch").click();
await expect(page.getByTitle("Current branch")).toContainText(
"orphaned-branch af3641c",
);
await expectCounts({ commits: 1, contributors: 1 }, page);
await expect(
page.locator("text=We couldn't find any files at this revision."),
).toBeVisible();
}
}
// Reset the source browser by clicking the project title.
{
await page.locator("text=source-browsing").click();
await expect(page.getByTitle("Change peer")).not.toContainText("alice");
await expect(page.getByTitle("Change peer")).not.toContainText("bob");
await expect(page.getByTitle("Current branch")).toContainText(
"main 530aabd",
);
await expect(page.locator("text=Git test repository")).toBeVisible();
}
// Bob's peer.
{
await page.getByTitle("Change peer").click();
await page.locator("text=bob").click();
await expect(page.getByTitle("Change peer")).toHaveText("bob");
await expect(page.getByTitle("Change peer")).not.toHaveText("delegate");
// Default `main` branch.
{
await expect(page.getByTitle("Current branch")).toContainText(
"main 0be0f03",
);
await expectCounts({ commits: 8, contributors: 2 }, page);
await expect(page.locator("text=0be0f03 Update readme")).toBeVisible();
}
}
});
test("clone modal", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByText("Clone").click();
await expect(
page.locator(
"text=rad clone rad://0.0.0.0/hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
),
).toBeVisible();
await expect(
page.locator(
"text=https://0.0.0.0/hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o.git",
),
).toBeVisible();
});
test("only one modal can be open at a time", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByTitle("Change peer").click();
await page.locator("text=alice hyn1mj").click();
await page.getByText("Clone").click();
await expect(page.locator("text=Code font")).not.toBeVisible();
await expect(page.locator("text=Use the Radicle CLI")).toBeVisible();
await expect(page.locator("text=bob hyy1k6g")).not.toBeVisible();
await expect(page.locator("text=feature/branch")).not.toBeVisible();
await page.getByTitle("Change branch").click();
await expect(page.locator("text=Code font")).not.toBeVisible();
await expect(page.locator("text=Use the Radicle CLI")).not.toBeVisible();
await expect(page.locator("text=bob hyy1k6g")).not.toBeVisible();
await expect(page.locator("text=feature/branch")).toBeVisible();
await page.getByTitle("Change peer").click();
await expect(page.locator("text=Code font")).not.toBeVisible();
await expect(page.locator("text=Use the Radicle CLI")).not.toBeVisible();
await expect(page.locator("text=bob hyy1k6g")).toBeVisible();
await expect(page.locator("text=feature/branch")).not.toBeVisible();
page.locator('button[name="Settings"]').click();
await expect(page.locator("text=Code font")).toBeVisible();
await expect(page.locator("text=Use the Radicle CLI")).not.toBeVisible();
await expect(page.locator("text=bob hyy1k6g")).not.toBeVisible();
await expect(page.locator("text=feature/branch")).not.toBeVisible();
});
+98
View File
@@ -0,0 +1,98 @@
import { test, expect } from "@tests/support/fixtures.js";
const sourceBrowsingFixture =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o";
const modifiedFileFixture = `${sourceBrowsingFixture}/remotes/hyy1k6ggg45pi7ip7ksyn1wt1ob4w5zh1awtg4qu3cxmbh5mws8pj1/commits/0be0f0302269b362be0bfe72aa4843eceaac5e3f`;
test("navigation from commit list", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.getByTitle("Change peer").click();
await page.locator("text=bob hyy1k6").click();
await page.locator('role=button[name="Commit count"]').click();
await page.locator("text=Update readme").click();
await expect(page).toHaveURL(modifiedFileFixture);
});
test("relative timestamps", async ({ page }) => {
page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
now: new Date("November 24 2022 12:00:00").valueOf(),
shouldClearNativeTimers: true,
shouldAdvanceTime: false,
});
};
});
await page.goto(modifiedFileFixture);
await expect(
page.locator(".commit header >> text=bob committed 3 days ago"),
).toBeVisible();
});
test("modified file", async ({ page }) => {
await page.goto(modifiedFileFixture);
// Commit header.
{
const header = page.locator(".commit header");
await expect(header.locator("text=Update readme")).toBeVisible();
await expect(header.locator("text=Verified")).toBeVisible();
await expect(
header.locator("text=0be0f0302269b362be0bfe72aa4843eceaac5e3f"),
).toBeVisible();
}
// Diff header.
await expect(
page.locator("text=1 file(s) changed with 1 addition(s) and 4 deletion(s)"),
).toBeVisible();
// Diff.
await expect(page.locator("text=- # Git test repository")).toBeVisible();
await expect(page.locator("text=+ Updated readme")).toBeVisible();
});
test("created file", async ({ page }) => {
await page.goto(
`${sourceBrowsingFixture}/remotes/hyn1mjueopwzrmb18c3zmgg8ei8qunn5wpg76ouymytfqkfxqx7bun/commits/d6318f7f3d9c15b8ac6dd52267c53220d00f0982`,
);
await expect(
page.locator("text=1 file(s) created with 9 addition(s) and 0 deletion(s)"),
).toBeVisible();
await expect(page.locator("text=subconscious.txt created")).toBeVisible();
});
test("deleted file", async ({ page }) => {
await page.goto(
`${sourceBrowsingFixture}/remotes/hyn1mjueopwzrmb18c3zmgg8ei8qunn5wpg76ouymytfqkfxqx7bun/commits/cd13c2d9a8a930d64a82b6134b44d1b872e33662`,
);
await expect(
page.locator("text=1 file(s) deleted with 0 addition(s) and 1 deletion(s)"),
).toBeVisible();
await expect(page.locator("text=.hidden deleted")).toBeVisible();
});
test("navigation to source tree at specific revision", async ({ page }) => {
await page.goto(
`${sourceBrowsingFixture}/commits/0801aceeab500033f8d608778218657bd626ef73`,
);
// Go to source tree at this revision.
await page.getByTitle("View file").click();
await expect(
page.locator("text=Add a deeply nested directory tree"),
).toBeVisible();
await expect(page).toHaveURL(
`${sourceBrowsingFixture}/tree/0801aceeab500033f8d608778218657bd626ef73/deep/directory/hierarchy/is/entirely/possible/in/git/repositories/.gitkeep`,
);
await expect(page.getByTitle("Current branch")).toContainText(
"0801aceeab500033f8d608778218657bd626ef73",
);
await expect(page.locator(".source-tree >> text=.gitkeep")).toBeVisible();
await expect(
page.locator(
"text=deep/directory/hierarchy/is/entirely/possible/in/git/repositories/",
),
).toBeVisible();
});
+129
View File
@@ -0,0 +1,129 @@
import { test, expect } from "@tests/support/fixtures.js";
const sourceBrowsingFixture =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o";
test("peer and branch switching", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.locator('role=button[name="Commit count"]').click();
// Alice's peer.
{
await page.getByTitle("Change peer").click();
await page.locator("text=alice hyn1mj").click();
await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
await expect(
page.locator(".commit-group-headers .commit-teaser"),
).toHaveCount(7);
const latestCommit = page.locator(".commit-teaser").first();
await expect(latestCommit).toContainText("Add Markdown cheat sheet");
await expect(latestCommit).toContainText("530aabd");
const earliestCommit = page.locator(".commit-teaser").last();
await expect(earliestCommit).toContainText(
"Initialize an empty git repository",
);
await expect(earliestCommit).toContainText("36d5bbe");
await page.getByTitle("Change branch").click();
await page.locator("text=feature/branch").click();
await expect(page.getByTitle("Current branch")).toContainText(
"feature/branch d6318f7",
);
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
await expect(page.locator(".commit-group-headers .commit")).toHaveCount(10);
await page.getByTitle("Change branch").click();
await page.locator("text=orphaned-branch").click();
await expect(page.getByTitle("Current branch")).toContainText(
"orphaned-branch af3641c",
);
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
await expect(
page.locator(".commit-group-headers .commit-teaser"),
).toHaveCount(1);
}
// Bob's peer.
{
await page.getByTitle("Change peer").click();
await page.locator("text=bob hyy1k6").click();
await expect(page.getByTitle("Change peer")).toHaveText("bob");
await expect(page.getByText("Monday, November 21, 2022")).toBeVisible();
await expect(
page.locator(".commit-group-headers").first().locator(".commit-teaser"),
).toHaveCount(1);
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
await expect(
page.locator(".commit-group-headers").last().locator(".commit-teaser"),
).toHaveCount(7);
await page.pause();
const latestCommit = page.locator(".commit-teaser").first();
await expect(latestCommit).toContainText("Update readme");
await expect(latestCommit).toContainText("0be0f03");
const earliestCommit = page.locator(".commit-teaser").last();
await expect(earliestCommit).toContainText(
"Initialize an empty git repository",
);
await expect(earliestCommit).toContainText("36d5bbe");
}
});
test("verified badge", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
await page.locator('role=button[name="Commit count"]').click();
await page.getByTitle("Change peer").click();
await page.locator("text=bob hyy1k6").click();
await expect(page.getByTitle("Change peer")).toHaveText("bob");
await page.locator("text=Verified").hover();
await expect(
page.locator(
"text=This commit was signed with the committer's radicle key.",
),
).toBeVisible();
await expect(
page.locator(
"text=bob committed hyy1k6ggg45pi7ip7ksyn1wt1ob4w5zh1awtg4qu3cxmbh5mws8pj1",
),
).toBeVisible();
});
test("relative timestamps", async ({ page }) => {
page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
now: new Date("November 24 2022 12:00:00").valueOf(),
shouldClearNativeTimers: true,
shouldAdvanceTime: false,
});
};
});
await page.goto(sourceBrowsingFixture);
await page.locator('role=button[name="Commit count"]').click();
await page.getByTitle("Change peer").click();
await page.locator("text=bob hyy1k6").click();
await expect(page.getByTitle("Change peer")).toHaveText("bob");
const latestCommit = page.locator(".commit-teaser").first();
await expect(latestCommit).toContainText("bob committed 3 days ago");
await expect(latestCommit).toContainText("0be0f03");
const earliestCommit = page.locator(".commit").last();
await expect(earliestCommit).toContainText(
"Alice Liddell committed 7 days ago",
);
});
+137
View File
@@ -0,0 +1,137 @@
import { test, expect } from "@tests/support/fixtures.js";
import {
expectBackAndForwardNavigationWorks,
expectUrlPersistsReload,
} from "@tests/support/router.js";
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.HASH_ROUTING = true;
});
});
test("navigate between landing and project page", async ({ page }) => {
await page.addInitScript(() => {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
},
reactions: [],
seeds: {
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
},
projects: {
pinned: [
{
name: "source-browsing",
urn: "rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
seed: "0.0.0.0",
},
],
},
};
});
await page.goto("/#/");
await expect(page).toHaveURL("/#/");
await page.locator("text=source-browsing").click();
await expect(page).toHaveURL(
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks("/#/", page);
await expectUrlPersistsReload(page);
});
test("navigation between seed and project pages", async ({ page }) => {
await page.goto("/#/seeds/radicle.local");
const project = page.locator(".project");
await project.click();
await expect(page).toHaveURL(
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks("/#/seeds/radicle.local", page);
await expectUrlPersistsReload(page);
await page.locator('role=button[name="Seed"]').click();
await expect(page).toHaveURL("/#/seeds/0.0.0.0");
});
test.describe("project page navigation", () => {
test("navigation between commit history and single commit", async ({
page,
}) => {
const projectHistoryURL =
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/history/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectHistoryURL);
await page.locator("text=Add Markdown cheat sheet").click();
await expect(page).toHaveURL(
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/commits/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks(projectHistoryURL, page);
await expectUrlPersistsReload(page);
});
test("navigate between tree and commit history", async ({ page }) => {
const projectTreeURL =
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator('role=button[name="Commit count"]').click();
await expect(page).toHaveURL(
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/history/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks(projectTreeURL, page);
await expectUrlPersistsReload(page);
});
test("navigate project paths", async ({ page }) => {
const projectTreeURL =
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator("text=.hidden").click();
await expect(page).toHaveURL(`${projectTreeURL}/.hidden`);
await page.locator("text=bin/").click();
await page.locator("text=true").click();
await expect(page).toHaveURL(`${projectTreeURL}/bin/true`);
await expectBackAndForwardNavigationWorks(
`${projectTreeURL}/.hidden`,
page,
);
await expectUrlPersistsReload(page);
});
test("navigate project paths with a selected peer", async ({ page }) => {
const projectTreeURL =
"/#/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/remotes/hyn1mjueopwzrmb18c3zmgg8ei8qunn5wpg76ouymytfqkfxqx7bun/tree";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator("text=.hidden").click();
await expect(page).toHaveURL(`${projectTreeURL}/main/.hidden`);
await page.locator("text=bin/").click();
await page.locator("text=true").click();
await expect(page).toHaveURL(`${projectTreeURL}/main/bin/true`);
await expectBackAndForwardNavigationWorks(
`${projectTreeURL}/main/.hidden`,
page,
);
await expectUrlPersistsReload(page);
});
});
+131
View File
@@ -0,0 +1,131 @@
import { test, expect } from "@tests/support/fixtures.js";
import {
expectBackAndForwardNavigationWorks,
expectUrlPersistsReload,
} from "@tests/support/router.js";
test("navigate between landing and project page", async ({ page }) => {
await page.addInitScript(() => {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
},
reactions: [],
seeds: {
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
},
projects: {
pinned: [
{
name: "source-browsing",
urn: "rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
seed: "0.0.0.0",
},
],
},
};
});
await page.goto("/");
await expect(page).toHaveURL("/");
await page.locator("text=source-browsing").click();
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks("/", page);
await expectUrlPersistsReload(page);
});
test("navigation between seed and project pages", async ({ page }) => {
await page.goto("/seeds/radicle.local");
const project = page.locator(".project");
await project.click();
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks("/seeds/radicle.local", page);
await expectUrlPersistsReload(page);
await page.locator('role=button[name="Seed"]').click();
await expect(page).toHaveURL("/seeds/0.0.0.0");
});
test.describe("project page navigation", () => {
test("navigation between commit history and single commit", async ({
page,
}) => {
const projectHistoryURL =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/history/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectHistoryURL);
await page.locator("text=Add Markdown cheat sheet").click();
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/commits/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks(projectHistoryURL, page);
await expectUrlPersistsReload(page);
});
test("navigate between tree and commit history", async ({ page }) => {
const projectTreeURL =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator('role=button[name="Commit count"]').click();
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/history/530aabdcc80397af254bc488b767169b92496e81",
);
await expectBackAndForwardNavigationWorks(projectTreeURL, page);
await expectUrlPersistsReload(page);
});
test("navigate project paths", async ({ page }) => {
const projectTreeURL =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/530aabdcc80397af254bc488b767169b92496e81";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator("text=.hidden").click();
await expect(page).toHaveURL(`${projectTreeURL}/.hidden`);
await page.locator("text=bin/").click();
await page.locator("text=true").click();
await expect(page).toHaveURL(`${projectTreeURL}/bin/true`);
await expectBackAndForwardNavigationWorks(
`${projectTreeURL}/.hidden`,
page,
);
await expectUrlPersistsReload(page);
});
test("navigate project paths with a selected peer", async ({ page }) => {
const projectTreeURL =
"seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/remotes/hyn1mjueopwzrmb18c3zmgg8ei8qunn5wpg76ouymytfqkfxqx7bun/tree";
await page.goto(projectTreeURL);
await expect(page).toHaveURL(projectTreeURL);
await page.locator("text=.hidden").click();
await expect(page).toHaveURL(`${projectTreeURL}/main/.hidden`);
await page.locator("text=bin/").click();
await page.locator("text=true").click();
await expect(page).toHaveURL(`${projectTreeURL}/main/bin/true`);
await expectBackAndForwardNavigationWorks(
`${projectTreeURL}/main/.hidden`,
page,
);
await expectUrlPersistsReload(page);
});
});
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from "@tests/support/fixtures.js";
test("navigate to existing project", async ({ page }) => {
await page.goto("/");
const searchInput = page.getByPlaceholder("Search a name or address…");
await searchInput.click();
await searchInput.fill("rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o");
await searchInput.press("Enter");
await expect(page).toHaveURL(
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree",
);
await expect(searchInput).not.toHaveValue(
"rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o",
);
});
test("navigate to a project that does not exist", async ({ page }) => {
await page.goto("/");
const searchInput = page.getByPlaceholder("Search a name or address…");
await searchInput.click();
await searchInput.fill("rad:git:hnrkn1ah5im83fwt4u3jfs5ndwpt9hrnm9wby");
await searchInput.press("Enter");
await page.waitForSelector(".search-bar.shaking");
await expect(page).toHaveURL("/");
await expect(searchInput).toHaveValue(
"rad:git:hnrkn1ah5im83fwt4u3jfs5ndwpt9hrnm9wby",
);
});
+46
View File
@@ -0,0 +1,46 @@
import { test, expect } from "@tests/support/fixtures.js";
test("seed metadata", async ({ page }) => {
await page.goto("/seeds/radicle.local");
await expect(page.locator("header").getByText("radicle.local")).toBeVisible();
await expect(
page.locator(".title >> text=radicle.local").getByRole("img"),
).toHaveAttribute("alt", "🚀");
await expect(page.getByRole("link", { name: "radicle.local" })).toBeVisible();
await expect(page.locator(".seed-address").getByRole("img")).toHaveAttribute(
"alt",
"🚀",
);
await expect(page.locator("text=hyb6i8…sg6nn4")).toBeVisible();
await expect(page.locator("text=8777")).toBeVisible();
await expect(page.locator("text=0.2.0")).toBeVisible();
});
test("seed projects", async ({ page }) => {
await page.goto("/seeds/radicle.local");
const project = page.locator(".project");
// Project metadata.
{
await expect(project.locator("text=source-browsing")).toBeVisible();
await expect(
project.locator("text=Git repository for source browsing tests"),
).toBeVisible();
await expect(
project.locator("text=530aabdcc80397af254bc488b767169b92496e81"),
).toBeVisible();
}
// Show project URN on hover.
{
await expect(
project.locator("text=rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o"),
).not.toBeVisible();
await project.hover();
await expect(
project.locator("text=rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o"),
).toBeVisible();
}
});
+75
View File
@@ -0,0 +1,75 @@
import { test, expect } from "@tests/support/fixtures.js";
const sourceBrowsingFixture =
"/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/main/src/true.c";
test("default settings", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
// Default settings.
{
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator("html")).toHaveAttribute(
"data-codefont",
"jetbrains",
);
}
});
test("settings persistance", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
page.locator('button[name="Settings"]').click();
await page.locator(".theme .toggle").click();
await page.getByText("Code font").click();
await page.getByText("System").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await expect(page.locator("html")).toHaveAttribute("data-codefont", "system");
page.reload();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await expect(page.locator("html")).toHaveAttribute("data-codefont", "system");
});
test("change theme", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
page.locator('button[name="Settings"]').click();
await page.locator(".theme .toggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await expect(page.locator("body")).toHaveCSS(
"background-color",
"rgb(243, 246, 253)",
);
// Source highlighting reacts to theme change.
await expect(page.getByText("() {")).toHaveCSS("color", "rgb(79, 91, 102)");
await page.locator(".theme .toggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator("body")).toHaveCSS(
"background-color",
"rgb(11, 19, 26)",
);
// Source highlighting reacts to theme change.
await expect(page.getByText("() {")).toHaveCSS("color", "rgb(192, 197, 206)");
});
test("change code font", async ({ page }) => {
await page.goto(sourceBrowsingFixture);
page.locator('button[name="Settings"]').click();
await page.getByText("Code font").click();
await page.getByText("System").click();
await expect(page.getByText("System")).toHaveClass(/active/);
await expect(page.locator("html")).toHaveAttribute("data-codefont", "system");
await page.getByText("JetBrains Mono").click();
await expect(page.getByText("JetBrains Mono")).toHaveClass(/active/);
await expect(page.locator("html")).toHaveAttribute(
"data-codefont",
"jetbrains",
);
});
Binary file not shown.
Binary file not shown.
+151
View File
@@ -0,0 +1,151 @@
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 * as logLabel from "@tests/support/logLabel.js";
export { expect };
export const test = base.extend<{
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
forAllTests: void;
customAppConfig: boolean;
stateDir: string;
outputLog: Stream.Writable;
}>({
customAppConfig: [false, { option: true }],
forAllTests: [
async ({ customAppConfig, outputLog, page }, use) => {
const browserLabel = logLabel.make("browser");
page.on("console", msg => {
// Ignore common console logs that we don't care about.
if (
msg
.text()
.startsWith(
`Module "buffer" has been externalized for browser compatibility.`,
) ||
msg.text().startsWith("[vite] connected.") ||
msg.text().startsWith("[vite] connecting...") ||
msg
.text()
.includes("Please make sure it wasn't preloaded for nothing.")
) {
return;
}
log(msg.text(), browserLabel, outputLog);
});
page.on("pageerror", msg => {
expect(
false,
`Test failed because there was a console error in the app: ${msg}`,
).toBeTruthy();
});
if (!customAppConfig) {
// Remember: `page.addInitScript()` is run in the browser which
// is completely isolated from the test environment, so we don't have
// access to any variables that we have in the test.
await page.addInitScript(() => {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
},
reactions: [],
seeds: {
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
},
projects: { pinned: [] },
};
});
}
const playwrightLabel = logLabel.make("playwright");
await page.route("**/*", route => {
if (
route.request().url().startsWith("http://127.0.0.1") ||
route.request().url().startsWith("http://localhost") ||
route.request().url().startsWith("http://0.0.0.0")
) {
return route.continue();
} else if (
route
.request()
.url()
.startsWith("https://www.gravatar.com/avatar/") ||
route.request().url().endsWith(".png")
) {
route.fulfill({
status: 200,
path: "./public/favicon.ico",
});
} else {
log(
`Aborted remote request: ${route.request().url()}`,
playwrightLabel,
outputLog,
);
return route.abort();
}
});
page.on("websocket", ws => {
log(`WebSocket opened: ${ws.url()}`, playwrightLabel, outputLog);
ws.on("framesent", event =>
log(
`WebSocket framesent: ${event.payload}`,
playwrightLabel,
outputLog,
),
);
ws.on("framereceived", event =>
log(
`WebSocket framereceived: ${event.payload}`,
playwrightLabel,
outputLog,
),
);
ws.on("close", () =>
log(`WebSocket closed`, playwrightLabel, outputLog),
);
});
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();
},
// 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") {
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);
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { FullConfig } from "@playwright/test";
export default async function globalSetup(_config: FullConfig): Promise<void> {
assertHttpApiRunning();
}
// Assert that the test http-api is running. If it is not running, throw an
// error that explains how to run it.
async function assertHttpApiRunning(): Promise<void> {
const palmTestFixtureSeedId =
"hyb6i8oggc3mgra9siy8yuohhtz34r98pcybja97c9o789wpsg6nn4";
const notRunningMessage =
"The http-api server with test fixtures needs to be running.\n" +
"👉 You can start it with `./scripts/run-http-api-with-fixtures`\n";
let peerId: string | undefined = undefined;
try {
const response = await fetch("http://0.0.0.0:8777");
const data = await response.json();
peerId = data.peer.id;
} catch (err) {
console.error(err);
throw new Error(notRunningMessage);
}
if (peerId !== palmTestFixtureSeedId) {
const wrongSeedMessage =
"The server on port 8777 doesn't have the right fixtures.\n";
throw new Error(wrongSeedMessage + notRunningMessage);
}
}
+42
View File
@@ -0,0 +1,42 @@
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",
];
const assignedColors: Record<string, ColorName> = {};
export function make(label: string): string {
if (assignedColors[label] === undefined) {
const color = availableColors.pop();
if (!color) {
throw new Error("We're out of colors. 🤷");
}
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)} | `,
);
}
+21
View File
@@ -0,0 +1,21 @@
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 expect(page).toHaveURL(beforeURL);
await page.goForward();
await expect(page).toHaveURL(currentURL);
};
View File
@@ -2,7 +2,7 @@ import type { Wallet } from "@app/wallet";
import { BigNumber } from "ethers";
import { describe, expect, test } from "vitest";
import * as utils from "./utils";
import * as utils from "@app/utils";
describe("Conversions", () => {
test("toWei", () => {
+4 -3
View File
@@ -1,11 +1,11 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src", "cypress/support"],
"include": ["src", "tests"],
"exclude": ["node_modules/*"],
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"types": ["svelte", "vite/client", "cypress"],
"types": ["svelte", "vite/client"],
"sourceMap": true,
"baseUrl": "./",
"moduleResolution": "node",
@@ -18,7 +18,8 @@
"skipLibCheck": true,
"paths": {
"@public/*": ["./public/*"],
"@app/*": ["./src/*"]
"@app/*": ["./src/*"],
"@tests/*": ["./tests/*"]
}
},
"noEmit": true
+23 -18
View File
@@ -1,10 +1,27 @@
///<reference types="vitest" />
import type { UserConfig } from "vite";
/// <reference types="vitest" />
import path from "path";
import pluginRewriteAll from "vite-plugin-rewrite-all";
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
const config: UserConfig = {
function defineConstants() {
const constants = {
VITEST: process.env.VITEST !== undefined,
PLAYWRIGHT: process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined,
};
// Don't overwrite HASH_ROUTING in Playwright tests, so we can control it
// from within the tests.
if (process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined) {
return constants;
} else {
// eslint-disable-next-line @typescript-eslint/naming-convention
return { ...constants, HASH_ROUTING: Boolean(process.env.HASH_ROUTING) };
}
}
export default defineConfig({
optimizeDeps: {
exclude: ["@pedrouid/environment", "@pedrouid/iso-crypto"],
},
@@ -14,7 +31,7 @@ const config: UserConfig = {
},
setupFiles: "./vitest/setupVitest",
environment: "happy-dom",
include: ["**/*.test.ts"],
include: ["tests/unit/**/*.test.ts"],
reporters: "verbose",
},
plugins: [
@@ -37,13 +54,6 @@ const config: UserConfig = {
"@app": path.resolve("./src"),
},
},
define: {
"process.env": {
// eslint-disable-next-line @typescript-eslint/naming-convention
READABLE_STREAM: "disable",
hashRouting: Boolean(process.env.HASH_ROUTING),
},
},
build: {
outDir: "build",
rollupOptions: {
@@ -58,11 +68,6 @@ const config: UserConfig = {
},
},
},
};
// For Vitest to work we need to unset READABLE_STREAM.
if (process.env.VITEST || process.env.Cypress) {
config.define = undefined;
}
export default config;
define: defineConstants(),
});