Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Replace Cypress with Playwright
Rūdolfs Ošiņš committed 3 years ago
commit 9cb9332e88e1cae69f54bb61c14a030e16a3763d
parent d740d7e5a1de92b4699f64c06ce0227920a032ae
85 files changed +2520 -4495
modified .github/workflows/check-build.yml
@@ -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/**/*
added .github/workflows/check-e2e.yml
@@ -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/**/*
modified .github/workflows/check-format.yml
@@ -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
deleted .github/workflows/check-integration-test.yml
@@ -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
-

modified .github/workflows/check-unit-test.yml
@@ -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
modified .github/workflows/check.yml
@@ -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
modified .gitignore
@@ -12,7 +12,8 @@ KaTeX_**.woff2
public/twemoji/*.svg

# Integration Tests
-
cypress/screenshots
+
tests/tmp/**/*
+
tests/artifacts/**/*

# Mac OS
.DS_Store
deleted cypress.config.ts
@@ -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",
-
  },
-
});
deleted cypress/e2e/home.spec.ts
@@ -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");
-
  });
-
});
deleted cypress/e2e/projectCommits.spec.ts
@@ -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",
-
    );
-
  });
-
});
deleted cypress/e2e/projectHeader.spec.ts
@@ -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");
-
  });
-
});
deleted cypress/e2e/projectRoot.spec.ts
@@ -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",
-
    );
-
  });
-
});
deleted cypress/fixtures/groupedCommits.json
@@ -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"
-
        }
-
      }
-
    ]
-
  }
-
]
deleted cypress/fixtures/projectActivity.json
@@ -1,3 +0,0 @@
-
{
-
  "activity": []
-
}
deleted cypress/fixtures/projectBranches.json
@@ -1,6 +0,0 @@
-
{
-
  "heads": {
-
    "master": "cbf5df499ab4f4a908f1756fbe2c236a4530516a",
-
    "main": "56e4e029c294b08546386e1fb706b772c7433c49"
-
  }
-
}
deleted cypress/fixtures/projectCommit.json
@@ -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"]
-
}
deleted cypress/fixtures/projectCommits.json
@@ -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
-
  }
-
}
deleted cypress/fixtures/projectHome.json
@@ -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"
-
    }
-
  ]
-
}
deleted cypress/fixtures/projectInfo.json
@@ -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
-
}
deleted cypress/fixtures/projectIssues.json
@@ -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
-
  }
-
]
deleted cypress/fixtures/projectList.json
@@ -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"
-
  }
-
]
deleted cypress/fixtures/projectPatches.json
@@ -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
-
  }
-
]
deleted cypress/fixtures/projectPeer.json
@@ -1 +0,0 @@
-
{ "id": "hyy841u4phudmr8s5rg1jjwd1ct7x7438wmjwtsm464y8uyxyhyi6c" }
deleted cypress/fixtures/projectReadme.json
@@ -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"
-
}
deleted cypress/fixtures/projectRemotes.json
@@ -1,7 +0,0 @@
-
[
-
  {
-
    "id": "hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke",
-
    "person": { "name": "dabit3" },
-
    "delegate": true
-
  }
-
]
deleted cypress/fixtures/projectTree56e4e02.json
@@ -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
-
  }
-
}
deleted cypress/fixtures/projectTreecbf5df4.json
@@ -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
-
  }
-
}
deleted cypress/support/component-index.html
@@ -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>
deleted cypress/support/component.ts
@@ -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);
deleted cypress/support/e2e.ts
@@ -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 };
modified package-lock.json
@@ -11,8 +11,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",
@@ -21,7 +19,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",
@@ -32,19 +30,26 @@
        "util": "^0.12.5"
      },
      "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",
@@ -54,64 +59,6 @@
        "node": ">=18.12.1"
      }
    },
-
    "node_modules/@colors/colors": {
-
      "version": "1.5.0",
-
      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
-
      "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
-
      "dev": true,
-
      "optional": true,
-
      "engines": {
-
        "node": ">=0.1.90"
-
      }
-
    },
-
    "node_modules/@cypress/request": {
-
      "version": "2.88.10",
-
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
-
      "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
-
      "dev": true,
-
      "dependencies": {
-
        "aws-sign2": "~0.7.0",
-
        "aws4": "^1.8.0",
-
        "caseless": "~0.12.0",
-
        "combined-stream": "~1.0.6",
-
        "extend": "~3.0.2",
-
        "forever-agent": "~0.6.1",
-
        "form-data": "~2.3.2",
-
        "http-signature": "~1.3.6",
-
        "is-typedarray": "~1.0.0",
-
        "isstream": "~0.1.2",
-
        "json-stringify-safe": "~5.0.1",
-
        "mime-types": "~2.1.19",
-
        "performance-now": "^2.1.0",
-
        "qs": "~6.5.2",
-
        "safe-buffer": "^5.1.2",
-
        "tough-cookie": "~2.5.0",
-
        "tunnel-agent": "^0.6.0",
-
        "uuid": "^8.3.2"
-
      },
-
      "engines": {
-
        "node": ">= 6"
-
      }
-
    },
-
    "node_modules/@cypress/xvfb": {
-
      "version": "1.2.4",
-
      "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
-
      "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
-
      "dev": true,
-
      "dependencies": {
-
        "debug": "^3.1.0",
-
        "lodash.once": "^4.1.1"
-
      }
-
    },
-
    "node_modules/@cypress/xvfb/node_modules/debug": {
-
      "version": "3.2.7",
-
      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-
      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "ms": "^2.1.1"
-
      }
-
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.15.10",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.10.tgz",
@@ -956,6 +903,22 @@
        "node": ">= 8"
      }
    },
+
    "node_modules/@playwright/test": {
+
      "version": "1.28.1",
+
      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz",
+
      "integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==",
+
      "dev": true,
+
      "dependencies": {
+
        "@types/node": "*",
+
        "playwright-core": "1.28.1"
+
      },
+
      "bin": {
+
        "playwright": "cli.js"
+
      },
+
      "engines": {
+
        "node": ">=14"
+
      }
+
    },
    "node_modules/@radicle/gray-matter": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/@radicle/gray-matter/-/gray-matter-4.1.0.tgz",
@@ -979,6 +942,24 @@
        "eth-sig-util": "^3.0.1"
      }
    },
+
    "node_modules/@sinonjs/commons": {
+
      "version": "2.0.0",
+
      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+
      "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+
      "dev": true,
+
      "dependencies": {
+
        "type-detect": "4.0.8"
+
      }
+
    },
+
    "node_modules/@sinonjs/fake-timers": {
+
      "version": "10.0.0",
+
      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.0.tgz",
+
      "integrity": "sha512-OjRc0IcyLLGLmu/vkJmqEYULU2mG/S7dLxPD+aONYWvTX7yia4mxKHs8Lz1ymfDv8KX3Adp/kRWUxi19ouaPsg==",
+
      "dev": true,
+
      "dependencies": {
+
        "@sinonjs/commons": "^2.0.0"
+
      }
+
    },
    "node_modules/@spruceid/siwe-parser": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.0.0.tgz",
@@ -1025,9 +1006,9 @@
      "integrity": "sha512-+fNbzyPb65oknwBgMjJrfs7dPXIJTDgnrFQcLI9+tpYTvHgrxwlqMm8geV4NA640qp+udIenWQDLU+hsB06Vcw=="
    },
    "node_modules/@sveltejs/vite-plugin-svelte": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.3.0.tgz",
-
      "integrity": "sha512-DZtl1qFT+re4HwEP8PjVZNIP7NO3Ua/akYpPteTXOT57PQNTXBK8pIAv4WwhQXMWIa8JcMNOnML95FR/Mhb3gw==",
+
      "version": "1.3.1",
+
      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.3.1.tgz",
+
      "integrity": "sha512-2Uu2sDdIR+XQWF7QWOVSF2jR9EU6Ciw1yWfYnfLYj8HIgnNxkh/8g22Fw2pBUI8QNyW/KxtqJUWBI+8ypamSrQ==",
      "dev": true,
      "dependencies": {
        "debug": "^4.3.4",
@@ -1035,7 +1016,7 @@
        "kleur": "^4.1.5",
        "magic-string": "^0.26.7",
        "svelte-hmr": "^0.15.1",
-
        "vitefu": "^0.2.1"
+
        "vitefu": "^0.2.2"
      },
      "engines": {
        "node": "^14.18.0 || >= 16"
@@ -1136,25 +1117,27 @@
      "dev": true
    },
    "node_modules/@types/lodash": {
-
      "version": "4.14.189",
-
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.189.tgz",
-
      "integrity": "sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==",
+
      "version": "4.14.190",
+
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.190.tgz",
+
      "integrity": "sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==",
      "dev": true
    },
    "node_modules/@types/marked": {
      "version": "4.0.7",
      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
-
      "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
+
      "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==",
+
      "dev": true
    },
    "node_modules/@types/md5": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
-
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og=="
+
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==",
+
      "dev": true
    },
    "node_modules/@types/node": {
-
      "version": "14.18.32",
-
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz",
-
      "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow=="
+
      "version": "18.11.9",
+
      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+
      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
    },
    "node_modules/@types/pbkdf2": {
      "version": "3.1.0",
@@ -1202,15 +1185,9 @@
      "dev": true
    },
    "node_modules/@types/sinonjs__fake-timers": {
-
      "version": "8.1.1",
-
      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
-
      "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
-
      "dev": true
-
    },
-
    "node_modules/@types/sizzle": {
-
      "version": "2.3.3",
-
      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
-
      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+
      "version": "8.1.2",
+
      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
+
      "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
      "dev": true
    },
    "node_modules/@types/trusted-types": {
@@ -1219,16 +1196,6 @@
      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
      "dev": true
    },
-
    "node_modules/@types/yauzl": {
-
      "version": "2.10.0",
-
      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
-
      "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
-
      "dev": true,
-
      "optional": true,
-
      "dependencies": {
-
        "@types/node": "*"
-
      }
-
    },
    "node_modules/@typescript-eslint/eslint-plugin": {
      "version": "5.44.0",
      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz",
@@ -1816,19 +1783,6 @@
        "node": ">= 6.0.0"
      }
    },
-
    "node_modules/aggregate-error": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
-
      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
-
      "dev": true,
-
      "dependencies": {
-
        "clean-stack": "^2.0.0",
-
        "indent-string": "^4.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/ajv": {
      "version": "6.12.6",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1845,42 +1799,6 @@
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
-
    "node_modules/ansi-colors": {
-
      "version": "4.1.3",
-
      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
-
      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      }
-
    },
-
    "node_modules/ansi-escapes": {
-
      "version": "4.3.2",
-
      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
-
      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "type-fest": "^0.21.3"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
-
    "node_modules/ansi-escapes/node_modules/type-fest": {
-
      "version": "0.21.3",
-
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
-
      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1923,26 +1841,6 @@
      "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.1.2.tgz",
      "integrity": "sha512-2OALKUe82NLVPe4NTooom8NykWIa2D7YxO7jG1pgnYWnkfhTUriXpITmLvVD8k8TzDfa9G5O4y8rPe2/uUB1Bg=="
    },
-
    "node_modules/arch": {
-
      "version": "2.2.0",
-
      "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
-
      "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
-
      "dev": true,
-
      "funding": [
-
        {
-
          "type": "github",
-
          "url": "https://github.com/sponsors/feross"
-
        },
-
        {
-
          "type": "patreon",
-
          "url": "https://www.patreon.com/feross"
-
        },
-
        {
-
          "type": "consulting",
-
          "url": "https://feross.org/support"
-
        }
-
      ]
-
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1965,24 +1863,6 @@
      "optional": true,
      "peer": true
    },
-
    "node_modules/asn1": {
-
      "version": "0.2.6",
-
      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-
      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "safer-buffer": "~2.1.0"
-
      }
-
    },
-
    "node_modules/assert-plus": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-
      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=0.8"
-
      }
-
    },
    "node_modules/assertion-error": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -1992,35 +1872,13 @@
        "node": "*"
      }
    },
-
    "node_modules/astral-regex": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
-
      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
-
    "node_modules/async": {
-
      "version": "3.2.4",
-
      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-
      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
-
      "dev": true
-
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-
      "dev": true
-
    },
-
    "node_modules/at-least-node": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
-
      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
      "dev": true,
-
      "engines": {
-
        "node": ">= 4.0.0"
-
      }
+
      "optional": true,
+
      "peer": true
    },
    "node_modules/available-typed-arrays": {
      "version": "1.0.5",
@@ -2033,21 +1891,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/aws-sign2": {
-
      "version": "0.7.0",
-
      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-
      "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-
      "dev": true,
-
      "engines": {
-
        "node": "*"
-
      }
-
    },
-
    "node_modules/aws4": {
-
      "version": "1.11.0",
-
      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
-
      "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
-
      "dev": true
-
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2081,21 +1924,6 @@
        }
      ]
    },
-
    "node_modules/bcrypt-pbkdf": {
-
      "version": "1.0.2",
-
      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-
      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-
      "dev": true,
-
      "dependencies": {
-
        "tweetnacl": "^0.14.3"
-
      }
-
    },
-
    "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": {
-
      "version": "0.14.5",
-
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-
      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-
      "dev": true
-
    },
    "node_modules/bech32": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
@@ -2116,18 +1944,6 @@
      "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==",
      "dev": true
    },
-
    "node_modules/blob-util": {
-
      "version": "2.0.2",
-
      "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
-
      "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
-
      "dev": true
-
    },
-
    "node_modules/bluebird": {
-
      "version": "3.7.2",
-
      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
-
      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
-
      "dev": true
-
    },
    "node_modules/bn.js": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
@@ -2238,15 +2054,6 @@
      "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
      "dev": true
    },
-
    "node_modules/cachedir": {
-
      "version": "2.3.0",
-
      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
-
      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      }
-
    },
    "node_modules/call-bind": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -2272,7 +2079,9 @@
      "version": "0.12.0",
      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
      "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "node_modules/chai": {
      "version": "4.3.6",
@@ -2293,33 +2102,17 @@
      }
    },
    "node_modules/chalk": {
-
      "version": "4.1.2",
-
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+
      "version": "5.1.2",
+
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
+
      "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==",
      "dev": true,
-
      "dependencies": {
-
        "ansi-styles": "^4.1.0",
-
        "supports-color": "^7.1.0"
-
      },
      "engines": {
-
        "node": ">=10"
+
        "node": "^12.17.0 || ^14.13 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
-
    "node_modules/chalk/node_modules/supports-color": {
-
      "version": "7.2.0",
-
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-
      "dev": true,
-
      "dependencies": {
-
        "has-flag": "^4.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/charenc": {
      "version": "0.0.2",
      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
@@ -2337,15 +2130,6 @@
        "node": "*"
      }
    },
-
    "node_modules/check-more-types": {
-
      "version": "2.24.0",
-
      "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
-
      "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">= 0.8.0"
-
      }
-
    },
    "node_modules/chokidar": {
      "version": "3.5.3",
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -2385,12 +2169,6 @@
        "node": ">= 6"
      }
    },
-
    "node_modules/ci-info": {
-
      "version": "3.5.0",
-
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
-
      "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==",
-
      "dev": true
-
    },
    "node_modules/cipher-base": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@@ -2401,58 +2179,6 @@
        "safe-buffer": "^5.0.1"
      }
    },
-
    "node_modules/clean-stack": {
-
      "version": "2.2.0",
-
      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
-
      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      }
-
    },
-
    "node_modules/cli-cursor": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
-
      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-
      "dev": true,
-
      "dependencies": {
-
        "restore-cursor": "^3.1.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
-
    "node_modules/cli-table3": {
-
      "version": "0.6.3",
-
      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz",
-
      "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==",
-
      "dev": true,
-
      "dependencies": {
-
        "string-width": "^4.2.0"
-
      },
-
      "engines": {
-
        "node": "10.* || >= 12.*"
-
      },
-
      "optionalDependencies": {
-
        "@colors/colors": "1.5.0"
-
      }
-
    },
-
    "node_modules/cli-truncate": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
-
      "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
-
      "dev": true,
-
      "dependencies": {
-
        "slice-ansi": "^3.0.0",
-
        "string-width": "^4.2.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2471,17 +2197,13 @@
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true
    },
-
    "node_modules/colorette": {
-
      "version": "2.0.19",
-
      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
-
      "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
-
      "dev": true
-
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
@@ -2489,24 +2211,6 @@
        "node": ">= 0.8"
      }
    },
-
    "node_modules/commander": {
-
      "version": "5.1.0",
-
      "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
-
      "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">= 6"
-
      }
-
    },
-
    "node_modules/common-tags": {
-
      "version": "1.8.2",
-
      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
-
      "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=4.0.0"
-
      }
-
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2578,7 +2282,9 @@
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "node_modules/create-hash": {
      "version": "1.2.0",
@@ -2667,110 +2373,17 @@
      "optional": true,
      "peer": true
    },
-
    "node_modules/cypress": {
-
      "version": "10.11.0",
-
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
-
      "integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
+
    "node_modules/data-urls": {
+
      "version": "3.0.2",
+
      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+
      "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
      "dev": true,
-
      "hasInstallScript": true,
+
      "optional": true,
+
      "peer": true,
      "dependencies": {
-
        "@cypress/request": "^2.88.10",
-
        "@cypress/xvfb": "^1.2.4",
-
        "@types/node": "^14.14.31",
-
        "@types/sinonjs__fake-timers": "8.1.1",
-
        "@types/sizzle": "^2.3.2",
-
        "arch": "^2.2.0",
-
        "blob-util": "^2.0.2",
-
        "bluebird": "^3.7.2",
-
        "buffer": "^5.6.0",
-
        "cachedir": "^2.3.0",
-
        "chalk": "^4.1.0",
-
        "check-more-types": "^2.24.0",
-
        "cli-cursor": "^3.1.0",
-
        "cli-table3": "~0.6.1",
-
        "commander": "^5.1.0",
-
        "common-tags": "^1.8.0",
-
        "dayjs": "^1.10.4",
-
        "debug": "^4.3.2",
-
        "enquirer": "^2.3.6",
-
        "eventemitter2": "6.4.7",
-
        "execa": "4.1.0",
-
        "executable": "^4.1.1",
-
        "extract-zip": "2.0.1",
-
        "figures": "^3.2.0",
-
        "fs-extra": "^9.1.0",
-
        "getos": "^3.2.1",
-
        "is-ci": "^3.0.0",
-
        "is-installed-globally": "~0.4.0",
-
        "lazy-ass": "^1.6.0",
-
        "listr2": "^3.8.3",
-
        "lodash": "^4.17.21",
-
        "log-symbols": "^4.0.0",
-
        "minimist": "^1.2.6",
-
        "ospath": "^1.2.2",
-
        "pretty-bytes": "^5.6.0",
-
        "proxy-from-env": "1.0.0",
-
        "request-progress": "^3.0.0",
-
        "semver": "^7.3.2",
-
        "supports-color": "^8.1.1",
-
        "tmp": "~0.2.1",
-
        "untildify": "^4.0.0",
-
        "yauzl": "^2.10.0"
-
      },
-
      "bin": {
-
        "cypress": "bin/cypress"
-
      },
-
      "engines": {
-
        "node": ">=12.0.0"
-
      }
-
    },
-
    "node_modules/cypress/node_modules/buffer": {
-
      "version": "5.7.1",
-
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-
      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-
      "dev": true,
-
      "funding": [
-
        {
-
          "type": "github",
-
          "url": "https://github.com/sponsors/feross"
-
        },
-
        {
-
          "type": "patreon",
-
          "url": "https://www.patreon.com/feross"
-
        },
-
        {
-
          "type": "consulting",
-
          "url": "https://feross.org/support"
-
        }
-
      ],
-
      "dependencies": {
-
        "base64-js": "^1.3.1",
-
        "ieee754": "^1.1.13"
-
      }
-
    },
-
    "node_modules/dashdash": {
-
      "version": "1.14.1",
-
      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-
      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-
      "dev": true,
-
      "dependencies": {
-
        "assert-plus": "^1.0.0"
-
      },
-
      "engines": {
-
        "node": ">=0.10"
-
      }
-
    },
-
    "node_modules/data-urls": {
-
      "version": "3.0.2",
-
      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
-
      "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
-
      "dev": true,
-
      "optional": true,
-
      "peer": true,
-
      "dependencies": {
-
        "abab": "^2.0.6",
-
        "whatwg-mimetype": "^3.0.0",
-
        "whatwg-url": "^11.0.0"
+
        "abab": "^2.0.6",
+
        "whatwg-mimetype": "^3.0.0",
+
        "whatwg-url": "^11.0.0"
      },
      "engines": {
        "node": ">=12"
@@ -2805,12 +2418,6 @@
        "node": ">=12"
      }
    },
-
    "node_modules/dayjs": {
-
      "version": "1.11.5",
-
      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
-
      "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
-
      "dev": true
-
    },
    "node_modules/debug": {
      "version": "4.3.4",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -2891,6 +2498,8 @@
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "engines": {
        "node": ">=0.4.0"
      }
@@ -2951,16 +2560,6 @@
      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
      "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
    },
-
    "node_modules/ecc-jsbn": {
-
      "version": "0.1.2",
-
      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-
      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-
      "dev": true,
-
      "dependencies": {
-
        "jsbn": "~0.1.0",
-
        "safer-buffer": "^2.1.0"
-
      }
-
    },
    "node_modules/elliptic": {
      "version": "6.5.4",
      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
@@ -2980,33 +2579,6 @@
      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
      "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
    },
-
    "node_modules/emoji-regex": {
-
      "version": "8.0.0",
-
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-
      "dev": true
-
    },
-
    "node_modules/end-of-stream": {
-
      "version": "1.4.4",
-
      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-
      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-
      "dev": true,
-
      "dependencies": {
-
        "once": "^1.4.0"
-
      }
-
    },
-
    "node_modules/enquirer": {
-
      "version": "2.3.6",
-
      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
-
      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-colors": "^4.1.1"
-
      },
-
      "engines": {
-
        "node": ">=8.6"
-
      }
-
    },
    "node_modules/entities": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
@@ -3657,6 +3229,22 @@
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      }
    },
+
    "node_modules/eslint/node_modules/chalk": {
+
      "version": "4.1.2",
+
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+
      "dev": true,
+
      "dependencies": {
+
        "ansi-styles": "^4.1.0",
+
        "supports-color": "^7.1.0"
+
      },
+
      "engines": {
+
        "node": ">=10"
+
      },
+
      "funding": {
+
        "url": "https://github.com/chalk/chalk?sponsor=1"
+
      }
+
    },
    "node_modules/eslint/node_modules/eslint-scope": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
@@ -3679,6 +3267,18 @@
        "node": ">=4.0"
      }
    },
+
    "node_modules/eslint/node_modules/supports-color": {
+
      "version": "7.2.0",
+
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+
      "dev": true,
+
      "dependencies": {
+
        "has-flag": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=8"
+
      }
+
    },
    "node_modules/espree": {
      "version": "9.4.0",
      "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
@@ -3920,12 +3520,6 @@
        "npm": ">=3"
      }
    },
-
    "node_modules/eventemitter2": {
-
      "version": "6.4.7",
-
      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
-
      "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
-
      "dev": true
-
    },
    "node_modules/events": {
      "version": "3.3.0",
      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -3944,47 +3538,6 @@
        "safe-buffer": "^5.1.1"
      }
    },
-
    "node_modules/execa": {
-
      "version": "4.1.0",
-
      "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
-
      "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
-
      "dev": true,
-
      "dependencies": {
-
        "cross-spawn": "^7.0.0",
-
        "get-stream": "^5.0.0",
-
        "human-signals": "^1.1.1",
-
        "is-stream": "^2.0.0",
-
        "merge-stream": "^2.0.0",
-
        "npm-run-path": "^4.0.0",
-
        "onetime": "^5.1.0",
-
        "signal-exit": "^3.0.2",
-
        "strip-final-newline": "^2.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-
      }
-
    },
-
    "node_modules/executable": {
-
      "version": "4.1.1",
-
      "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
-
      "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
-
      "dev": true,
-
      "dependencies": {
-
        "pify": "^2.2.0"
-
      },
-
      "engines": {
-
        "node": ">=4"
-
      }
-
    },
-
    "node_modules/extend": {
-
      "version": "3.0.2",
-
      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-
      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-
      "dev": true
-
    },
    "node_modules/extend-shallow": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@@ -3996,35 +3549,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/extract-zip": {
-
      "version": "2.0.1",
-
      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
-
      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
-
      "dev": true,
-
      "dependencies": {
-
        "debug": "^4.1.1",
-
        "get-stream": "^5.1.0",
-
        "yauzl": "^2.10.0"
-
      },
-
      "bin": {
-
        "extract-zip": "cli.js"
-
      },
-
      "engines": {
-
        "node": ">= 10.17.0"
-
      },
-
      "optionalDependencies": {
-
        "@types/yauzl": "^2.9.1"
-
      }
-
    },
-
    "node_modules/extsprintf": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-
      "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-
      "dev": true,
-
      "engines": [
-
        "node >=0.6.0"
-
      ]
-
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4080,39 +3604,6 @@
        "reusify": "^1.0.4"
      }
    },
-
    "node_modules/fd-slicer": {
-
      "version": "1.1.0",
-
      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
-
      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
-
      "dev": true,
-
      "dependencies": {
-
        "pend": "~1.2.0"
-
      }
-
    },
-
    "node_modules/figures": {
-
      "version": "3.2.0",
-
      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-
      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
-
      "dev": true,
-
      "dependencies": {
-
        "escape-string-regexp": "^1.0.5"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
-
    "node_modules/figures/node_modules/escape-string-regexp": {
-
      "version": "1.0.5",
-
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-
      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=0.8.0"
-
      }
-
    },
    "node_modules/file-entry-cache": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -4180,20 +3671,13 @@
        "is-callable": "^1.1.3"
      }
    },
-
    "node_modules/forever-agent": {
-
      "version": "0.6.1",
-
      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-
      "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-
      "dev": true,
-
      "engines": {
-
        "node": "*"
-
      }
-
    },
    "node_modules/form-data": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.6",
@@ -4203,21 +3687,6 @@
        "node": ">= 0.12"
      }
    },
-
    "node_modules/fs-extra": {
-
      "version": "9.1.0",
-
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-
      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "at-least-node": "^1.0.0",
-
        "graceful-fs": "^4.2.0",
-
        "jsonfile": "^6.0.1",
-
        "universalify": "^2.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      }
-
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4300,21 +3769,6 @@
        "node": ">=4"
      }
    },
-
    "node_modules/get-stream": {
-
      "version": "5.2.0",
-
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
-
      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
-
      "dev": true,
-
      "dependencies": {
-
        "pump": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/get-symbol-description": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
@@ -4330,24 +3784,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/getos": {
-
      "version": "3.2.1",
-
      "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
-
      "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
-
      "dev": true,
-
      "dependencies": {
-
        "async": "^3.2.0"
-
      }
-
    },
-
    "node_modules/getpass": {
-
      "version": "0.1.7",
-
      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-
      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-
      "dev": true,
-
      "dependencies": {
-
        "assert-plus": "^1.0.0"
-
      }
-
    },
    "node_modules/glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4379,21 +3815,6 @@
        "node": ">=10.13.0"
      }
    },
-
    "node_modules/global-dirs": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
-
      "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
-
      "dev": true,
-
      "dependencies": {
-
        "ini": "2.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/globals": {
      "version": "13.17.0",
      "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
@@ -4631,20 +4052,6 @@
      "optional": true,
      "peer": true
    },
-
    "node_modules/http-signature": {
-
      "version": "1.3.6",
-
      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
-
      "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
-
      "dev": true,
-
      "dependencies": {
-
        "assert-plus": "^1.0.0",
-
        "jsprim": "^2.0.2",
-
        "sshpk": "^1.14.1"
-
      },
-
      "engines": {
-
        "node": ">=0.10"
-
      }
-
    },
    "node_modules/https-proxy-agent": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -4660,15 +4067,6 @@
        "node": ">= 6"
      }
    },
-
    "node_modules/human-signals": {
-
      "version": "1.1.1",
-
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
-
      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8.12.0"
-
      }
-
    },
    "node_modules/iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -4736,15 +4134,6 @@
        "node": ">=0.8.19"
      }
    },
-
    "node_modules/indent-string": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-
      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4759,15 +4148,6 @@
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
    },
-
    "node_modules/ini": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
-
      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=10"
-
      }
-
    },
    "node_modules/internal-slot": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
@@ -4850,18 +4230,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/is-ci": {
-
      "version": "3.0.1",
-
      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
-
      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "ci-info": "^3.2.0"
-
      },
-
      "bin": {
-
        "is-ci": "bin.js"
-
      }
-
    },
    "node_modules/is-core-module": {
      "version": "2.10.0",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
@@ -4905,15 +4273,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/is-fullwidth-code-point": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/is-generator-function": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -4950,22 +4309,6 @@
        "npm": ">=3"
      }
    },
-
    "node_modules/is-installed-globally": {
-
      "version": "0.4.0",
-
      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
-
      "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "global-dirs": "^3.0.0",
-
        "is-path-inside": "^3.0.2"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/is-negative-zero": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -5043,18 +4386,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/is-stream": {
-
      "version": "2.0.1",
-
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-
      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/is-string": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -5106,18 +4437,6 @@
      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
    },
-
    "node_modules/is-unicode-supported": {
-
      "version": "0.1.0",
-
      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-
      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/is-weakref": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -5143,12 +4462,6 @@
      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
      "dev": true
    },
-
    "node_modules/isstream": {
-
      "version": "0.1.2",
-
      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-
      "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
-
      "dev": true
-
    },
    "node_modules/js-sdsl": {
      "version": "4.1.5",
      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -5171,12 +4484,6 @@
        "js-yaml": "bin/js-yaml.js"
      }
    },
-
    "node_modules/jsbn": {
-
      "version": "0.1.1",
-
      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-
      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-
      "dev": true
-
    },
    "node_modules/jsdom": {
      "version": "20.0.2",
      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.2.tgz",
@@ -5320,12 +4627,6 @@
        }
      }
    },
-
    "node_modules/json-schema": {
-
      "version": "0.4.0",
-
      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
-
      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-
      "dev": true
-
    },
    "node_modules/json-schema-traverse": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5338,39 +4639,6 @@
      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
      "dev": true
    },
-
    "node_modules/json-stringify-safe": {
-
      "version": "5.0.1",
-
      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-
      "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-
      "dev": true
-
    },
-
    "node_modules/jsonfile": {
-
      "version": "6.1.0",
-
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-
      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "universalify": "^2.0.0"
-
      },
-
      "optionalDependencies": {
-
        "graceful-fs": "^4.1.6"
-
      }
-
    },
-
    "node_modules/jsprim": {
-
      "version": "2.0.2",
-
      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
-
      "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
-
      "dev": true,
-
      "engines": [
-
        "node >=0.6.0"
-
      ],
-
      "dependencies": {
-
        "assert-plus": "1.0.0",
-
        "extsprintf": "1.3.0",
-
        "json-schema": "0.4.0",
-
        "verror": "1.10.0"
-
      }
-
    },
    "node_modules/katex": {
      "version": "0.16.3",
      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.3.tgz",
@@ -5430,15 +4698,6 @@
        "node": ">=6"
      }
    },
-
    "node_modules/lazy-ass": {
-
      "version": "1.6.0",
-
      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
-
      "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
-
      "dev": true,
-
      "engines": {
-
        "node": "> 0.8"
-
      }
-
    },
    "node_modules/levn": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5452,33 +4711,6 @@
        "node": ">= 0.8.0"
      }
    },
-
    "node_modules/listr2": {
-
      "version": "3.14.0",
-
      "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
-
      "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
-
      "dev": true,
-
      "dependencies": {
-
        "cli-truncate": "^2.1.0",
-
        "colorette": "^2.0.16",
-
        "log-update": "^4.0.0",
-
        "p-map": "^4.0.0",
-
        "rfdc": "^1.3.0",
-
        "rxjs": "^7.5.1",
-
        "through": "^2.3.8",
-
        "wrap-ansi": "^7.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10.0.0"
-
      },
-
      "peerDependencies": {
-
        "enquirer": ">= 2.3.0 < 3"
-
      },
-
      "peerDependenciesMeta": {
-
        "enquirer": {
-
          "optional": true
-
        }
-
      }
-
    },
    "node_modules/local-pkg": {
      "version": "0.4.2",
      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
@@ -5517,77 +4749,6 @@
      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
      "dev": true
    },
-
    "node_modules/lodash.once": {
-
      "version": "4.1.1",
-
      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
-
      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
-
      "dev": true
-
    },
-
    "node_modules/log-symbols": {
-
      "version": "4.1.0",
-
      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-
      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
-
      "dev": true,
-
      "dependencies": {
-
        "chalk": "^4.1.0",
-
        "is-unicode-supported": "^0.1.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
-
    "node_modules/log-update": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
-
      "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-escapes": "^4.3.0",
-
        "cli-cursor": "^3.1.0",
-
        "slice-ansi": "^4.0.0",
-
        "wrap-ansi": "^6.2.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
-
    "node_modules/log-update/node_modules/slice-ansi": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
-
      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-styles": "^4.0.0",
-
        "astral-regex": "^2.0.0",
-
        "is-fullwidth-code-point": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
-
      }
-
    },
-
    "node_modules/log-update/node_modules/wrap-ansi": {
-
      "version": "6.2.0",
-
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-
      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-styles": "^4.0.0",
-
        "string-width": "^4.1.0",
-
        "strip-ansi": "^6.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/loupe": {
      "version": "2.3.4",
      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
@@ -5618,9 +4779,9 @@
      }
    },
    "node_modules/marked": {
-
      "version": "4.2.2",
-
      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz",
-
      "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==",
+
      "version": "4.2.3",
+
      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
+
      "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==",
      "bin": {
        "marked": "bin/marked.js"
      },
@@ -5649,12 +4810,6 @@
        "safe-buffer": "^5.1.2"
      }
    },
-
    "node_modules/merge-stream": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-
      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-
      "dev": true
-
    },
    "node_modules/merge2": {
      "version": "1.4.1",
      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5682,6 +4837,8 @@
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "engines": {
        "node": ">= 0.6"
      }
@@ -5691,6 +4848,8 @@
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "dependencies": {
        "mime-db": "1.52.0"
      },
@@ -5698,15 +4857,6 @@
        "node": ">= 0.6"
      }
    },
-
    "node_modules/mimic-fn": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-
      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      }
-
    },
    "node_modules/min-indent": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -5840,18 +4990,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/npm-run-path": {
-
      "version": "4.0.1",
-
      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-
      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
-
      "dev": true,
-
      "dependencies": {
-
        "path-key": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/nwsapi": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
@@ -5901,21 +5039,6 @@
        "wrappy": "1"
      }
    },
-
    "node_modules/onetime": {
-
      "version": "5.1.2",
-
      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-
      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-
      "dev": true,
-
      "dependencies": {
-
        "mimic-fn": "^2.1.0"
-
      },
-
      "engines": {
-
        "node": ">=6"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/optionator": {
      "version": "0.9.1",
      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -5933,12 +5056,6 @@
        "node": ">= 0.8.0"
      }
    },
-
    "node_modules/ospath": {
-
      "version": "1.2.2",
-
      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
-
      "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
-
      "dev": true
-
    },
    "node_modules/p-limit": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -5969,21 +5086,6 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
-
    "node_modules/p-map": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
-
      "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "aggregate-error": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/parent-module": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6084,18 +5186,6 @@
        "node": ">=0.12"
      }
    },
-
    "node_modules/pend": {
-
      "version": "1.2.0",
-
      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
-
      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
-
      "dev": true
-
    },
-
    "node_modules/performance-now": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-
      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-
      "dev": true
-
    },
    "node_modules/picocolors": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -6114,15 +5204,6 @@
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
-
    "node_modules/pify": {
-
      "version": "2.3.0",
-
      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-
      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=0.10.0"
-
      }
-
    },
    "node_modules/plausible-tracker": {
      "version": "0.3.8",
      "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz",
@@ -6131,6 +5212,18 @@
        "node": ">=10"
      }
    },
+
    "node_modules/playwright-core": {
+
      "version": "1.28.1",
+
      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
+
      "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
+
      "dev": true,
+
      "bin": {
+
        "playwright": "cli.js"
+
      },
+
      "engines": {
+
        "node": ">=14"
+
      }
+
    },
    "node_modules/postcss": {
      "version": "8.4.18",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
@@ -6189,18 +5282,6 @@
        "svelte": "^3.2.0"
      }
    },
-
    "node_modules/pretty-bytes": {
-
      "version": "5.6.0",
-
      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
-
      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    },
    "node_modules/process-nextick-args": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -6220,27 +5301,13 @@
        "asap": "~2.0.6"
      }
    },
-
    "node_modules/proxy-from-env": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
-
      "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
-
      "dev": true
-
    },
    "node_modules/psl": {
      "version": "1.9.0",
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
-
      "dev": true
-
    },
-
    "node_modules/pump": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-
      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
      "dev": true,
-
      "dependencies": {
-
        "end-of-stream": "^1.1.0",
-
        "once": "^1.3.1"
-
      }
+
      "optional": true,
+
      "peer": true
    },
    "node_modules/punycode": {
      "version": "2.1.1",
@@ -6260,6 +5327,8 @@
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "engines": {
        "node": ">=0.6"
      }
@@ -6369,15 +5438,6 @@
        "url": "https://github.com/sponsors/mysticatea"
      }
    },
-
    "node_modules/request-progress": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
-
      "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
-
      "dev": true,
-
      "dependencies": {
-
        "throttleit": "^1.0.0"
-
      }
-
    },
    "node_modules/requires-port": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -6412,19 +5472,6 @@
        "node": ">=4"
      }
    },
-
    "node_modules/restore-cursor": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
-
      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
-
      "dev": true,
-
      "dependencies": {
-
        "onetime": "^5.1.0",
-
        "signal-exit": "^3.0.2"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/reusify": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -6435,12 +5482,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/rfdc": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
-
      "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
-
      "dev": true
-
    },
    "node_modules/rimraf": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -6516,15 +5557,6 @@
        "queue-microtask": "^1.2.2"
      }
    },
-
    "node_modules/rxjs": {
-
      "version": "7.5.7",
-
      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
-
      "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
-
      "dev": true,
-
      "dependencies": {
-
        "tslib": "^2.1.0"
-
      }
-
    },
    "node_modules/sade": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -6573,7 +5605,9 @@
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "node_modules/sander": {
      "version": "0.5.1",
@@ -6723,12 +5757,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/signal-exit": {
-
      "version": "3.0.7",
-
      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-
      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-
      "dev": true
-
    },
    "node_modules/siwe": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/siwe/-/siwe-2.0.5.tgz",
@@ -6752,20 +5780,6 @@
        "node": ">=8"
      }
    },
-
    "node_modules/slice-ansi": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
-
      "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-styles": "^4.0.0",
-
        "astral-regex": "^2.0.0",
-
        "is-fullwidth-code-point": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/sorcery": {
      "version": "0.10.0",
      "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
@@ -6811,37 +5825,6 @@
        "node": ">=6"
      }
    },
-
    "node_modules/sshpk": {
-
      "version": "1.17.0",
-
      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
-
      "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "asn1": "~0.2.3",
-
        "assert-plus": "^1.0.0",
-
        "bcrypt-pbkdf": "^1.0.0",
-
        "dashdash": "^1.12.0",
-
        "ecc-jsbn": "~0.1.1",
-
        "getpass": "^0.1.1",
-
        "jsbn": "~0.1.0",
-
        "safer-buffer": "^2.0.2",
-
        "tweetnacl": "~0.14.0"
-
      },
-
      "bin": {
-
        "sshpk-conv": "bin/sshpk-conv",
-
        "sshpk-sign": "bin/sshpk-sign",
-
        "sshpk-verify": "bin/sshpk-verify"
-
      },
-
      "engines": {
-
        "node": ">=0.10.0"
-
      }
-
    },
-
    "node_modules/sshpk/node_modules/tweetnacl": {
-
      "version": "0.14.5",
-
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-
      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-
      "dev": true
-
    },
    "node_modules/strict-uri-encode": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -6858,20 +5841,6 @@
        "safe-buffer": "~5.2.0"
      }
    },
-
    "node_modules/string-width": {
-
      "version": "4.2.3",
-
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-
      "dev": true,
-
      "dependencies": {
-
        "emoji-regex": "^8.0.0",
-
        "is-fullwidth-code-point": "^3.0.0",
-
        "strip-ansi": "^6.0.1"
-
      },
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/string.prototype.trimend": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
@@ -6918,15 +5887,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/strip-final-newline": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-
      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=6"
-
      }
-
    },
    "node_modules/strip-hex-prefix": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz",
@@ -6975,21 +5935,6 @@
        "url": "https://github.com/sponsors/antfu"
      }
    },
-
    "node_modules/supports-color": {
-
      "version": "8.1.1",
-
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-
      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-
      "dev": true,
-
      "dependencies": {
-
        "has-flag": "^4.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/chalk/supports-color?sponsor=1"
-
      }
-
    },
    "node_modules/supports-preserve-symlinks-flag": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -7190,18 +6135,6 @@
      "optional": true,
      "peer": true
    },
-
    "node_modules/throttleit": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
-
      "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
-
      "dev": true
-
    },
-
    "node_modules/through": {
-
      "version": "2.3.8",
-
      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-
      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
-
      "dev": true
-
    },
    "node_modules/tinybench": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz",
@@ -7226,18 +6159,6 @@
        "node": ">=14.0.0"
      }
    },
-
    "node_modules/tmp": {
-
      "version": "0.2.1",
-
      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
-
      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "rimraf": "^3.0.0"
-
      },
-
      "engines": {
-
        "node": ">=8.17.0"
-
      }
-
    },
    "node_modules/to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7250,19 +6171,6 @@
        "node": ">=8.0"
      }
    },
-
    "node_modules/tough-cookie": {
-
      "version": "2.5.0",
-
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-
      "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-
      "dev": true,
-
      "dependencies": {
-
        "psl": "^1.1.28",
-
        "punycode": "^2.1.1"
-
      },
-
      "engines": {
-
        "node": ">=0.8"
-
      }
-
    },
    "node_modules/tr46": {
      "version": "0.0.3",
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -7272,9 +6180,9 @@
      "peer": true
    },
    "node_modules/tslib": {
-
      "version": "2.4.0",
-
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
-
      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+
      "version": "2.4.1",
+
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+
      "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
      "dev": true
    },
    "node_modules/tsutils": {
@@ -7298,18 +6206,6 @@
      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
      "dev": true
    },
-
    "node_modules/tunnel-agent": {
-
      "version": "0.6.0",
-
      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-
      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-
      "dev": true,
-
      "dependencies": {
-
        "safe-buffer": "^5.0.1"
-
      },
-
      "engines": {
-
        "node": "*"
-
      }
-
    },
    "node_modules/tweetnacl": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
@@ -7454,24 +6350,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
-
    "node_modules/universalify": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-
      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">= 10.0.0"
-
      }
-
    },
-
    "node_modules/untildify": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
-
      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
-
      "dev": true,
-
      "engines": {
-
        "node": ">=8"
-
      }
-
    },
    "node_modules/uri-js": {
      "version": "4.4.1",
      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -7509,34 +6387,11 @@
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
    },
-
    "node_modules/uuid": {
-
      "version": "8.3.2",
-
      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-
      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-
      "dev": true,
-
      "bin": {
-
        "uuid": "dist/bin/uuid"
-
      }
-
    },
    "node_modules/valid-url": {
      "version": "1.0.9",
      "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
      "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="
    },
-
    "node_modules/verror": {
-
      "version": "1.10.0",
-
      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-
      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-
      "dev": true,
-
      "engines": [
-
        "node >=0.6.0"
-
      ],
-
      "dependencies": {
-
        "assert-plus": "^1.0.0",
-
        "core-util-is": "1.0.2",
-
        "extsprintf": "^1.2.0"
-
      }
-
    },
    "node_modules/vite": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
@@ -7602,9 +6457,9 @@
      }
    },
    "node_modules/vitefu": {
-
      "version": "0.2.1",
-
      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.1.tgz",
-
      "integrity": "sha512-clkvXTAeUf+XQKm3bhWUhT4pye+3acm6YCTGaWhxxIvZZ/QjnA3JA8Zud+z/mO5y5XYvJJhevs5Sjkv/FI8nRw==",
+
      "version": "0.2.2",
+
      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.2.tgz",
+
      "integrity": "sha512-8CKEIWPm4B4DUDN+h+hVJa9pyNi7rzc5MYmbxhs1wcMakueGFNWB5/DL30USm9qU3xUPnL4/rrLEAwwFiD1tag==",
      "dev": true,
      "peerDependencies": {
        "vite": "^3.0.0"
@@ -7798,23 +6653,6 @@
        "node": ">=0.10.0"
      }
    },
-
    "node_modules/wrap-ansi": {
-
      "version": "7.0.0",
-
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-
      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-
      "dev": true,
-
      "dependencies": {
-
        "ansi-styles": "^4.0.0",
-
        "string-width": "^4.1.0",
-
        "strip-ansi": "^6.0.0"
-
      },
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-
      }
-
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -7865,84 +6703,20 @@
      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
      "dev": true
    },
-
    "node_modules/yauzl": {
-
      "version": "2.10.0",
-
      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
-
      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
-
      "dev": true,
-
      "dependencies": {
-
        "buffer-crc32": "~0.2.3",
-
        "fd-slicer": "~1.1.0"
-
      }
-
    },
    "node_modules/yocto-queue": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
      "dev": true,
-
      "engines": {
-
        "node": ">=10"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/sindresorhus"
-
      }
-
    }
-
  },
-
  "dependencies": {
-
    "@colors/colors": {
-
      "version": "1.5.0",
-
      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
-
      "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
-
      "dev": true,
-
      "optional": true
-
    },
-
    "@cypress/request": {
-
      "version": "2.88.10",
-
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
-
      "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
-
      "dev": true,
-
      "requires": {
-
        "aws-sign2": "~0.7.0",
-
        "aws4": "^1.8.0",
-
        "caseless": "~0.12.0",
-
        "combined-stream": "~1.0.6",
-
        "extend": "~3.0.2",
-
        "forever-agent": "~0.6.1",
-
        "form-data": "~2.3.2",
-
        "http-signature": "~1.3.6",
-
        "is-typedarray": "~1.0.0",
-
        "isstream": "~0.1.2",
-
        "json-stringify-safe": "~5.0.1",
-
        "mime-types": "~2.1.19",
-
        "performance-now": "^2.1.0",
-
        "qs": "~6.5.2",
-
        "safe-buffer": "^5.1.2",
-
        "tough-cookie": "~2.5.0",
-
        "tunnel-agent": "^0.6.0",
-
        "uuid": "^8.3.2"
-
      }
-
    },
-
    "@cypress/xvfb": {
-
      "version": "1.2.4",
-
      "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
-
      "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
-
      "dev": true,
-
      "requires": {
-
        "debug": "^3.1.0",
-
        "lodash.once": "^4.1.1"
+
      "engines": {
+
        "node": ">=10"
      },
-
      "dependencies": {
-
        "debug": {
-
          "version": "3.2.7",
-
          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-
          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-
          "dev": true,
-
          "requires": {
-
            "ms": "^2.1.1"
-
          }
-
        }
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
      }
-
    },
+
    }
+
  },
+
  "dependencies": {
    "@esbuild/android-arm": {
      "version": "0.15.10",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.10.tgz",
@@ -8431,6 +7205,16 @@
        "fastq": "^1.6.0"
      }
    },
+
    "@playwright/test": {
+
      "version": "1.28.1",
+
      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz",
+
      "integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==",
+
      "dev": true,
+
      "requires": {
+
        "@types/node": "*",
+
        "playwright-core": "1.28.1"
+
      }
+
    },
    "@radicle/gray-matter": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/@radicle/gray-matter/-/gray-matter-4.1.0.tgz",
@@ -8451,6 +7235,24 @@
        "eth-sig-util": "^3.0.1"
      }
    },
+
    "@sinonjs/commons": {
+
      "version": "2.0.0",
+
      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+
      "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+
      "dev": true,
+
      "requires": {
+
        "type-detect": "4.0.8"
+
      }
+
    },
+
    "@sinonjs/fake-timers": {
+
      "version": "10.0.0",
+
      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.0.tgz",
+
      "integrity": "sha512-OjRc0IcyLLGLmu/vkJmqEYULU2mG/S7dLxPD+aONYWvTX7yia4mxKHs8Lz1ymfDv8KX3Adp/kRWUxi19ouaPsg==",
+
      "dev": true,
+
      "requires": {
+
        "@sinonjs/commons": "^2.0.0"
+
      }
+
    },
    "@spruceid/siwe-parser": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.0.0.tgz",
@@ -8494,9 +7296,9 @@
      "integrity": "sha512-+fNbzyPb65oknwBgMjJrfs7dPXIJTDgnrFQcLI9+tpYTvHgrxwlqMm8geV4NA640qp+udIenWQDLU+hsB06Vcw=="
    },
    "@sveltejs/vite-plugin-svelte": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.3.0.tgz",
-
      "integrity": "sha512-DZtl1qFT+re4HwEP8PjVZNIP7NO3Ua/akYpPteTXOT57PQNTXBK8pIAv4WwhQXMWIa8JcMNOnML95FR/Mhb3gw==",
+
      "version": "1.3.1",
+
      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.3.1.tgz",
+
      "integrity": "sha512-2Uu2sDdIR+XQWF7QWOVSF2jR9EU6Ciw1yWfYnfLYj8HIgnNxkh/8g22Fw2pBUI8QNyW/KxtqJUWBI+8ypamSrQ==",
      "dev": true,
      "requires": {
        "debug": "^4.3.4",
@@ -8504,7 +7306,7 @@
        "kleur": "^4.1.5",
        "magic-string": "^0.26.7",
        "svelte-hmr": "^0.15.1",
-
        "vitefu": "^0.2.1"
+
        "vitefu": "^0.2.2"
      }
    },
    "@tootallnate/once": {
@@ -8589,25 +7391,27 @@
      "dev": true
    },
    "@types/lodash": {
-
      "version": "4.14.189",
-
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.189.tgz",
-
      "integrity": "sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==",
+
      "version": "4.14.190",
+
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.190.tgz",
+
      "integrity": "sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==",
      "dev": true
    },
    "@types/marked": {
      "version": "4.0.7",
      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
-
      "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
+
      "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==",
+
      "dev": true
    },
    "@types/md5": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
-
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og=="
+
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==",
+
      "dev": true
    },
    "@types/node": {
-
      "version": "14.18.32",
-
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz",
-
      "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow=="
+
      "version": "18.11.9",
+
      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+
      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
    },
    "@types/pbkdf2": {
      "version": "3.1.0",
@@ -8655,15 +7459,9 @@
      "dev": true
    },
    "@types/sinonjs__fake-timers": {
-
      "version": "8.1.1",
-
      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
-
      "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
-
      "dev": true
-
    },
-
    "@types/sizzle": {
-
      "version": "2.3.3",
-
      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
-
      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+
      "version": "8.1.2",
+
      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
+
      "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
      "dev": true
    },
    "@types/trusted-types": {
@@ -8672,16 +7470,6 @@
      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
      "dev": true
    },
-
    "@types/yauzl": {
-
      "version": "2.10.0",
-
      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
-
      "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
-
      "dev": true,
-
      "optional": true,
-
      "requires": {
-
        "@types/node": "*"
-
      }
-
    },
    "@typescript-eslint/eslint-plugin": {
      "version": "5.44.0",
      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz",
@@ -9094,16 +7882,6 @@
        "debug": "4"
      }
    },
-
    "aggregate-error": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
-
      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
-
      "dev": true,
-
      "requires": {
-
        "clean-stack": "^2.0.0",
-
        "indent-string": "^4.0.0"
-
      }
-
    },
    "ajv": {
      "version": "6.12.6",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -9116,29 +7894,6 @@
        "uri-js": "^4.2.2"
      }
    },
-
    "ansi-colors": {
-
      "version": "4.1.3",
-
      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
-
      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
-
      "dev": true
-
    },
-
    "ansi-escapes": {
-
      "version": "4.3.2",
-
      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
-
      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
-
      "dev": true,
-
      "requires": {
-
        "type-fest": "^0.21.3"
-
      },
-
      "dependencies": {
-
        "type-fest": {
-
          "version": "0.21.3",
-
          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
-
          "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
-
          "dev": true
-
        }
-
      }
-
    },
    "ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -9169,12 +7924,6 @@
      "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.1.2.tgz",
      "integrity": "sha512-2OALKUe82NLVPe4NTooom8NykWIa2D7YxO7jG1pgnYWnkfhTUriXpITmLvVD8k8TzDfa9G5O4y8rPe2/uUB1Bg=="
    },
-
    "arch": {
-
      "version": "2.2.0",
-
      "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
-
      "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
-
      "dev": true
-
    },
    "argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -9194,68 +7943,25 @@
      "optional": true,
      "peer": true
    },
-
    "asn1": {
-
      "version": "0.2.6",
-
      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-
      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-
      "dev": true,
-
      "requires": {
-
        "safer-buffer": "~2.1.0"
-
      }
-
    },
-
    "assert-plus": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-
      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-
      "dev": true
-
    },
    "assertion-error": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
      "dev": true
    },
-
    "astral-regex": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
-
      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
-
      "dev": true
-
    },
-
    "async": {
-
      "version": "3.2.4",
-
      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-
      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
-
      "dev": true
-
    },
    "asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-
      "dev": true
-
    },
-
    "at-least-node": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
-
      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "available-typed-arrays": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
    },
-
    "aws-sign2": {
-
      "version": "0.7.0",
-
      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-
      "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-
      "dev": true
-
    },
-
    "aws4": {
-
      "version": "1.11.0",
-
      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
-
      "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
-
      "dev": true
-
    },
    "balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -9275,23 +7981,6 @@
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
    },
-
    "bcrypt-pbkdf": {
-
      "version": "1.0.2",
-
      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-
      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-
      "dev": true,
-
      "requires": {
-
        "tweetnacl": "^0.14.3"
-
      },
-
      "dependencies": {
-
        "tweetnacl": {
-
          "version": "0.14.5",
-
          "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-
          "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-
          "dev": true
-
        }
-
      }
-
    },
    "bech32": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
@@ -9309,18 +7998,6 @@
      "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==",
      "dev": true
    },
-
    "blob-util": {
-
      "version": "2.0.2",
-
      "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
-
      "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
-
      "dev": true
-
    },
-
    "bluebird": {
-
      "version": "3.7.2",
-
      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
-
      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
-
      "dev": true
-
    },
    "bn.js": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
@@ -9411,12 +8088,6 @@
      "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
      "dev": true
    },
-
    "cachedir": {
-
      "version": "2.3.0",
-
      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
-
      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
-
      "dev": true
-
    },
    "call-bind": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -9436,7 +8107,9 @@
      "version": "0.12.0",
      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
      "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "chai": {
      "version": "4.3.6",
@@ -9454,25 +8127,10 @@
      }
    },
    "chalk": {
-
      "version": "4.1.2",
-
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-
      "dev": true,
-
      "requires": {
-
        "ansi-styles": "^4.1.0",
-
        "supports-color": "^7.1.0"
-
      },
-
      "dependencies": {
-
        "supports-color": {
-
          "version": "7.2.0",
-
          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-
          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-
          "dev": true,
-
          "requires": {
-
            "has-flag": "^4.0.0"
-
          }
-
        }
-
      }
+
      "version": "5.1.2",
+
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
+
      "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==",
+
      "dev": true
    },
    "charenc": {
      "version": "0.0.2",
@@ -9485,12 +8143,6 @@
      "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
      "dev": true
    },
-
    "check-more-types": {
-
      "version": "2.24.0",
-
      "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
-
      "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
-
      "dev": true
-
    },
    "chokidar": {
      "version": "3.5.3",
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -9518,12 +8170,6 @@
        }
      }
    },
-
    "ci-info": {
-
      "version": "3.5.0",
-
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
-
      "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==",
-
      "dev": true
-
    },
    "cipher-base": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@@ -9534,41 +8180,6 @@
        "safe-buffer": "^5.0.1"
      }
    },
-
    "clean-stack": {
-
      "version": "2.2.0",
-
      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
-
      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
-
      "dev": true
-
    },
-
    "cli-cursor": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
-
      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-
      "dev": true,
-
      "requires": {
-
        "restore-cursor": "^3.1.0"
-
      }
-
    },
-
    "cli-table3": {
-
      "version": "0.6.3",
-
      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz",
-
      "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==",
-
      "dev": true,
-
      "requires": {
-
        "@colors/colors": "1.5.0",
-
        "string-width": "^4.2.0"
-
      }
-
    },
-
    "cli-truncate": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
-
      "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
-
      "dev": true,
-
      "requires": {
-
        "slice-ansi": "^3.0.0",
-
        "string-width": "^4.2.0"
-
      }
-
    },
    "color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -9584,33 +8195,17 @@
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true
    },
-
    "colorette": {
-
      "version": "2.0.19",
-
      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
-
      "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
-
      "dev": true
-
    },
    "combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "requires": {
        "delayed-stream": "~1.0.0"
      }
    },
-
    "commander": {
-
      "version": "5.1.0",
-
      "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
-
      "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
-
      "dev": true
-
    },
-
    "common-tags": {
-
      "version": "1.8.2",
-
      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
-
      "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
-
      "dev": true
-
    },
    "concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -9678,7 +8273,9 @@
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "create-hash": {
      "version": "1.2.0",
@@ -9760,77 +8357,6 @@
        }
      }
    },
-
    "cypress": {
-
      "version": "10.11.0",
-
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
-
      "integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
-
      "dev": true,
-
      "requires": {
-
        "@cypress/request": "^2.88.10",
-
        "@cypress/xvfb": "^1.2.4",
-
        "@types/node": "^14.14.31",
-
        "@types/sinonjs__fake-timers": "8.1.1",
-
        "@types/sizzle": "^2.3.2",
-
        "arch": "^2.2.0",
-
        "blob-util": "^2.0.2",
-
        "bluebird": "^3.7.2",
-
        "buffer": "^5.6.0",
-
        "cachedir": "^2.3.0",
-
        "chalk": "^4.1.0",
-
        "check-more-types": "^2.24.0",
-
        "cli-cursor": "^3.1.0",
-
        "cli-table3": "~0.6.1",
-
        "commander": "^5.1.0",
-
        "common-tags": "^1.8.0",
-
        "dayjs": "^1.10.4",
-
        "debug": "^4.3.2",
-
        "enquirer": "^2.3.6",
-
        "eventemitter2": "6.4.7",
-
        "execa": "4.1.0",
-
        "executable": "^4.1.1",
-
        "extract-zip": "2.0.1",
-
        "figures": "^3.2.0",
-
        "fs-extra": "^9.1.0",
-
        "getos": "^3.2.1",
-
        "is-ci": "^3.0.0",
-
        "is-installed-globally": "~0.4.0",
-
        "lazy-ass": "^1.6.0",
-
        "listr2": "^3.8.3",
-
        "lodash": "^4.17.21",
-
        "log-symbols": "^4.0.0",
-
        "minimist": "^1.2.6",
-
        "ospath": "^1.2.2",
-
        "pretty-bytes": "^5.6.0",
-
        "proxy-from-env": "1.0.0",
-
        "request-progress": "^3.0.0",
-
        "semver": "^7.3.2",
-
        "supports-color": "^8.1.1",
-
        "tmp": "~0.2.1",
-
        "untildify": "^4.0.0",
-
        "yauzl": "^2.10.0"
-
      },
-
      "dependencies": {
-
        "buffer": {
-
          "version": "5.7.1",
-
          "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-
          "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-
          "dev": true,
-
          "requires": {
-
            "base64-js": "^1.3.1",
-
            "ieee754": "^1.1.13"
-
          }
-
        }
-
      }
-
    },
-
    "dashdash": {
-
      "version": "1.14.1",
-
      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-
      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-
      "dev": true,
-
      "requires": {
-
        "assert-plus": "^1.0.0"
-
      }
-
    },
    "data-urls": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -9869,12 +8395,6 @@
        }
      }
    },
-
    "dayjs": {
-
      "version": "1.11.5",
-
      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
-
      "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
-
      "dev": true
-
    },
    "debug": {
      "version": "4.3.4",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -9931,7 +8451,9 @@
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "detect-browser": {
      "version": "5.2.0",
@@ -9977,16 +8499,6 @@
      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
      "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
    },
-
    "ecc-jsbn": {
-
      "version": "0.1.2",
-
      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-
      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-
      "dev": true,
-
      "requires": {
-
        "jsbn": "~0.1.0",
-
        "safer-buffer": "^2.1.0"
-
      }
-
    },
    "elliptic": {
      "version": "6.5.4",
      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
@@ -10008,30 +8520,6 @@
        }
      }
    },
-
    "emoji-regex": {
-
      "version": "8.0.0",
-
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-
      "dev": true
-
    },
-
    "end-of-stream": {
-
      "version": "1.4.4",
-
      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-
      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-
      "dev": true,
-
      "requires": {
-
        "once": "^1.4.0"
-
      }
-
    },
-
    "enquirer": {
-
      "version": "2.3.6",
-
      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
-
      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
-
      "dev": true,
-
      "requires": {
-
        "ansi-colors": "^4.1.1"
-
      }
-
    },
    "entities": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
@@ -10381,6 +8869,16 @@
        "text-table": "^0.2.0"
      },
      "dependencies": {
+
        "chalk": {
+
          "version": "4.1.2",
+
          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+
          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+
          "dev": true,
+
          "requires": {
+
            "ansi-styles": "^4.1.0",
+
            "supports-color": "^7.1.0"
+
          }
+
        },
        "eslint-scope": {
          "version": "7.1.1",
          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
@@ -10396,6 +8894,15 @@
          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
          "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
          "dev": true
+
        },
+
        "supports-color": {
+
          "version": "7.2.0",
+
          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+
          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+
          "dev": true,
+
          "requires": {
+
            "has-flag": "^4.0.0"
+
          }
        }
      }
    },
@@ -10642,12 +9149,6 @@
        "strip-hex-prefix": "1.0.0"
      }
    },
-
    "eventemitter2": {
-
      "version": "6.4.7",
-
      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
-
      "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
-
      "dev": true
-
    },
    "events": {
      "version": "3.3.0",
      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -10663,38 +9164,6 @@
        "safe-buffer": "^5.1.1"
      }
    },
-
    "execa": {
-
      "version": "4.1.0",
-
      "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
-
      "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
-
      "dev": true,
-
      "requires": {
-
        "cross-spawn": "^7.0.0",
-
        "get-stream": "^5.0.0",
-
        "human-signals": "^1.1.1",
-
        "is-stream": "^2.0.0",
-
        "merge-stream": "^2.0.0",
-
        "npm-run-path": "^4.0.0",
-
        "onetime": "^5.1.0",
-
        "signal-exit": "^3.0.2",
-
        "strip-final-newline": "^2.0.0"
-
      }
-
    },
-
    "executable": {
-
      "version": "4.1.1",
-
      "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
-
      "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
-
      "dev": true,
-
      "requires": {
-
        "pify": "^2.2.0"
-
      }
-
    },
-
    "extend": {
-
      "version": "3.0.2",
-
      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-
      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-
      "dev": true
-
    },
    "extend-shallow": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@@ -10703,24 +9172,6 @@
        "is-extendable": "^0.1.0"
      }
    },
-
    "extract-zip": {
-
      "version": "2.0.1",
-
      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
-
      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
-
      "dev": true,
-
      "requires": {
-
        "@types/yauzl": "^2.9.1",
-
        "debug": "^4.1.1",
-
        "get-stream": "^5.1.0",
-
        "yauzl": "^2.10.0"
-
      }
-
    },
-
    "extsprintf": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-
      "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-
      "dev": true
-
    },
    "fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -10772,32 +9223,6 @@
        "reusify": "^1.0.4"
      }
    },
-
    "fd-slicer": {
-
      "version": "1.1.0",
-
      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
-
      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
-
      "dev": true,
-
      "requires": {
-
        "pend": "~1.2.0"
-
      }
-
    },
-
    "figures": {
-
      "version": "3.2.0",
-
      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-
      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
-
      "dev": true,
-
      "requires": {
-
        "escape-string-regexp": "^1.0.5"
-
      },
-
      "dependencies": {
-
        "escape-string-regexp": {
-
          "version": "1.0.5",
-
          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-
          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-
          "dev": true
-
        }
-
      }
-
    },
    "file-entry-cache": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -10850,33 +9275,17 @@
        "is-callable": "^1.1.3"
      }
    },
-
    "forever-agent": {
-
      "version": "0.6.1",
-
      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-
      "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-
      "dev": true
-
    },
    "form-data": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "requires": {
-
        "asynckit": "^0.4.0",
-
        "combined-stream": "^1.0.6",
-
        "mime-types": "^2.1.12"
-
      }
-
    },
-
    "fs-extra": {
-
      "version": "9.1.0",
-
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-
      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-
      "dev": true,
-
      "requires": {
-
        "at-least-node": "^1.0.0",
-
        "graceful-fs": "^4.2.0",
-
        "jsonfile": "^6.0.1",
-
        "universalify": "^2.0.0"
+
        "asynckit": "^0.4.0",
+
        "combined-stream": "^1.0.6",
+
        "mime-types": "^2.1.12"
      }
    },
    "fs.realpath": {
@@ -10936,15 +9345,6 @@
      "optional": true,
      "peer": true
    },
-
    "get-stream": {
-
      "version": "5.2.0",
-
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
-
      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
-
      "dev": true,
-
      "requires": {
-
        "pump": "^3.0.0"
-
      }
-
    },
    "get-symbol-description": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
@@ -10954,24 +9354,6 @@
        "get-intrinsic": "^1.1.1"
      }
    },
-
    "getos": {
-
      "version": "3.2.1",
-
      "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
-
      "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
-
      "dev": true,
-
      "requires": {
-
        "async": "^3.2.0"
-
      }
-
    },
-
    "getpass": {
-
      "version": "0.1.7",
-
      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-
      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-
      "dev": true,
-
      "requires": {
-
        "assert-plus": "^1.0.0"
-
      }
-
    },
    "glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -10994,15 +9376,6 @@
        "is-glob": "^4.0.3"
      }
    },
-
    "global-dirs": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
-
      "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
-
      "dev": true,
-
      "requires": {
-
        "ini": "2.0.0"
-
      }
-
    },
    "globals": {
      "version": "13.17.0",
      "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
@@ -11191,17 +9564,6 @@
        }
      }
    },
-
    "http-signature": {
-
      "version": "1.3.6",
-
      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
-
      "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
-
      "dev": true,
-
      "requires": {
-
        "assert-plus": "^1.0.0",
-
        "jsprim": "^2.0.2",
-
        "sshpk": "^1.14.1"
-
      }
-
    },
    "https-proxy-agent": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -11214,12 +9576,6 @@
        "debug": "4"
      }
    },
-
    "human-signals": {
-
      "version": "1.1.1",
-
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
-
      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
-
      "dev": true
-
    },
    "iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -11258,12 +9614,6 @@
      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
      "dev": true
    },
-
    "indent-string": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-
      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-
      "dev": true
-
    },
    "inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -11278,12 +9628,6 @@
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
    },
-
    "ini": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
-
      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
-
      "dev": true
-
    },
    "internal-slot": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
@@ -11339,15 +9683,6 @@
      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="
    },
-
    "is-ci": {
-
      "version": "3.0.1",
-
      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
-
      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
-
      "dev": true,
-
      "requires": {
-
        "ci-info": "^3.2.0"
-
      }
-
    },
    "is-core-module": {
      "version": "2.10.0",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
@@ -11376,12 +9711,6 @@
      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
      "dev": true
    },
-
    "is-fullwidth-code-point": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-
      "dev": true
-
    },
    "is-generator-function": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -11405,16 +9734,6 @@
      "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==",
      "dev": true
    },
-
    "is-installed-globally": {
-
      "version": "0.4.0",
-
      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
-
      "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
-
      "dev": true,
-
      "requires": {
-
        "global-dirs": "^3.0.0",
-
        "is-path-inside": "^3.0.2"
-
      }
-
    },
    "is-negative-zero": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -11465,12 +9784,6 @@
        "call-bind": "^1.0.2"
      }
    },
-
    "is-stream": {
-
      "version": "2.0.1",
-
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-
      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-
      "dev": true
-
    },
    "is-string": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -11504,12 +9817,6 @@
      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
    },
-
    "is-unicode-supported": {
-
      "version": "0.1.0",
-
      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-
      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
-
      "dev": true
-
    },
    "is-weakref": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -11532,12 +9839,6 @@
      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
      "dev": true
    },
-
    "isstream": {
-
      "version": "0.1.2",
-
      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-
      "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
-
      "dev": true
-
    },
    "js-sdsl": {
      "version": "4.1.5",
      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -11557,12 +9858,6 @@
        "argparse": "^2.0.1"
      }
    },
-
    "jsbn": {
-
      "version": "0.1.1",
-
      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-
      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-
      "dev": true
-
    },
    "jsdom": {
      "version": "20.0.2",
      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.2.tgz",
@@ -11668,12 +9963,6 @@
        }
      }
    },
-
    "json-schema": {
-
      "version": "0.4.0",
-
      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
-
      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-
      "dev": true
-
    },
    "json-schema-traverse": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -11686,34 +9975,6 @@
      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
      "dev": true
    },
-
    "json-stringify-safe": {
-
      "version": "5.0.1",
-
      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-
      "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-
      "dev": true
-
    },
-
    "jsonfile": {
-
      "version": "6.1.0",
-
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-
      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-
      "dev": true,
-
      "requires": {
-
        "graceful-fs": "^4.1.6",
-
        "universalify": "^2.0.0"
-
      }
-
    },
-
    "jsprim": {
-
      "version": "2.0.2",
-
      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
-
      "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
-
      "dev": true,
-
      "requires": {
-
        "assert-plus": "1.0.0",
-
        "extsprintf": "1.3.0",
-
        "json-schema": "0.4.0",
-
        "verror": "1.10.0"
-
      }
-
    },
    "katex": {
      "version": "0.16.3",
      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.3.tgz",
@@ -11755,12 +10016,6 @@
      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
      "dev": true
    },
-
    "lazy-ass": {
-
      "version": "1.6.0",
-
      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
-
      "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
-
      "dev": true
-
    },
    "levn": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -11771,22 +10026,6 @@
        "type-check": "~0.4.0"
      }
    },
-
    "listr2": {
-
      "version": "3.14.0",
-
      "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
-
      "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
-
      "dev": true,
-
      "requires": {
-
        "cli-truncate": "^2.1.0",
-
        "colorette": "^2.0.16",
-
        "log-update": "^4.0.0",
-
        "p-map": "^4.0.0",
-
        "rfdc": "^1.3.0",
-
        "rxjs": "^7.5.1",
-
        "through": "^2.3.8",
-
        "wrap-ansi": "^7.0.0"
-
      }
-
    },
    "local-pkg": {
      "version": "0.4.2",
      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
@@ -11813,58 +10052,6 @@
      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
      "dev": true
    },
-
    "lodash.once": {
-
      "version": "4.1.1",
-
      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
-
      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
-
      "dev": true
-
    },
-
    "log-symbols": {
-
      "version": "4.1.0",
-
      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-
      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
-
      "dev": true,
-
      "requires": {
-
        "chalk": "^4.1.0",
-
        "is-unicode-supported": "^0.1.0"
-
      }
-
    },
-
    "log-update": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
-
      "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
-
      "dev": true,
-
      "requires": {
-
        "ansi-escapes": "^4.3.0",
-
        "cli-cursor": "^3.1.0",
-
        "slice-ansi": "^4.0.0",
-
        "wrap-ansi": "^6.2.0"
-
      },
-
      "dependencies": {
-
        "slice-ansi": {
-
          "version": "4.0.0",
-
          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
-
          "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
-
          "dev": true,
-
          "requires": {
-
            "ansi-styles": "^4.0.0",
-
            "astral-regex": "^2.0.0",
-
            "is-fullwidth-code-point": "^3.0.0"
-
          }
-
        },
-
        "wrap-ansi": {
-
          "version": "6.2.0",
-
          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-
          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
-
          "dev": true,
-
          "requires": {
-
            "ansi-styles": "^4.0.0",
-
            "string-width": "^4.1.0",
-
            "strip-ansi": "^6.0.0"
-
          }
-
        }
-
      }
-
    },
    "loupe": {
      "version": "2.3.4",
      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
@@ -11889,9 +10076,9 @@
      }
    },
    "marked": {
-
      "version": "4.2.2",
-
      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz",
-
      "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ=="
+
      "version": "4.2.3",
+
      "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
+
      "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw=="
    },
    "md5": {
      "version": "2.3.0",
@@ -11914,12 +10101,6 @@
        "safe-buffer": "^5.1.2"
      }
    },
-
    "merge-stream": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-
      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-
      "dev": true
-
    },
    "merge2": {
      "version": "1.4.1",
      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -11940,23 +10121,21 @@
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "requires": {
        "mime-db": "1.52.0"
      }
    },
-
    "mimic-fn": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-
      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-
      "dev": true
-
    },
    "min-indent": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -12050,15 +10229,6 @@
      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
      "dev": true
    },
-
    "npm-run-path": {
-
      "version": "4.0.1",
-
      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-
      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
-
      "dev": true,
-
      "requires": {
-
        "path-key": "^3.0.0"
-
      }
-
    },
    "nwsapi": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
@@ -12096,15 +10266,6 @@
        "wrappy": "1"
      }
    },
-
    "onetime": {
-
      "version": "5.1.2",
-
      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-
      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-
      "dev": true,
-
      "requires": {
-
        "mimic-fn": "^2.1.0"
-
      }
-
    },
    "optionator": {
      "version": "0.9.1",
      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -12119,12 +10280,6 @@
        "word-wrap": "^1.2.3"
      }
    },
-
    "ospath": {
-
      "version": "1.2.2",
-
      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
-
      "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
-
      "dev": true
-
    },
    "p-limit": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -12143,15 +10298,6 @@
        "p-limit": "^3.0.2"
      }
    },
-
    "p-map": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
-
      "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
-
      "dev": true,
-
      "requires": {
-
        "aggregate-error": "^3.0.0"
-
      }
-
    },
    "parent-module": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -12228,18 +10374,6 @@
        "sha.js": "^2.4.8"
      }
    },
-
    "pend": {
-
      "version": "1.2.0",
-
      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
-
      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
-
      "dev": true
-
    },
-
    "performance-now": {
-
      "version": "2.1.0",
-
      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-
      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-
      "dev": true
-
    },
    "picocolors": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -12252,17 +10386,17 @@
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
      "dev": true
    },
-
    "pify": {
-
      "version": "2.3.0",
-
      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-
      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-
      "dev": true
-
    },
    "plausible-tracker": {
      "version": "0.3.8",
      "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz",
      "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg=="
    },
+
    "playwright-core": {
+
      "version": "1.28.1",
+
      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
+
      "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
+
      "dev": true
+
    },
    "postcss": {
      "version": "8.4.18",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
@@ -12293,12 +10427,6 @@
      "dev": true,
      "requires": {}
    },
-
    "pretty-bytes": {
-
      "version": "5.6.0",
-
      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
-
      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
-
      "dev": true
-
    },
    "process-nextick-args": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -12318,27 +10446,13 @@
        "asap": "~2.0.6"
      }
    },
-
    "proxy-from-env": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
-
      "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
-
      "dev": true
-
    },
    "psl": {
      "version": "1.9.0",
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
-
      "dev": true
-
    },
-
    "pump": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-
      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
      "dev": true,
-
      "requires": {
-
        "end-of-stream": "^1.1.0",
-
        "once": "^1.3.1"
-
      }
+
      "optional": true,
+
      "peer": true
    },
    "punycode": {
      "version": "2.1.1",
@@ -12354,7 +10468,9 @@
      "version": "6.5.3",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "query-string": {
      "version": "6.13.5",
@@ -12423,15 +10539,6 @@
      "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
      "dev": true
    },
-
    "request-progress": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
-
      "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
-
      "dev": true,
-
      "requires": {
-
        "throttleit": "^1.0.0"
-
      }
-
    },
    "requires-port": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -12457,28 +10564,12 @@
      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
      "dev": true
    },
-
    "restore-cursor": {
-
      "version": "3.1.0",
-
      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
-
      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
-
      "dev": true,
-
      "requires": {
-
        "onetime": "^5.1.0",
-
        "signal-exit": "^3.0.2"
-
      }
-
    },
    "reusify": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
      "dev": true
    },
-
    "rfdc": {
-
      "version": "1.3.0",
-
      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
-
      "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
-
      "dev": true
-
    },
    "rimraf": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -12525,15 +10616,6 @@
        "queue-microtask": "^1.2.2"
      }
    },
-
    "rxjs": {
-
      "version": "7.5.7",
-
      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
-
      "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
-
      "dev": true,
-
      "requires": {
-
        "tslib": "^2.1.0"
-
      }
-
    },
    "sade": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -12562,7 +10644,9 @@
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-
      "dev": true
+
      "dev": true,
+
      "optional": true,
+
      "peer": true
    },
    "sander": {
      "version": "0.5.1",
@@ -12682,12 +10766,6 @@
        "object-inspect": "^1.9.0"
      }
    },
-
    "signal-exit": {
-
      "version": "3.0.7",
-
      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-
      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-
      "dev": true
-
    },
    "siwe": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/siwe/-/siwe-2.0.5.tgz",
@@ -12705,17 +10783,6 @@
      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
      "dev": true
    },
-
    "slice-ansi": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
-
      "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
-
      "dev": true,
-
      "requires": {
-
        "ansi-styles": "^4.0.0",
-
        "astral-regex": "^2.0.0",
-
        "is-fullwidth-code-point": "^3.0.0"
-
      }
-
    },
    "sorcery": {
      "version": "0.10.0",
      "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
@@ -12749,31 +10816,6 @@
      "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
      "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
    },
-
    "sshpk": {
-
      "version": "1.17.0",
-
      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
-
      "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
-
      "dev": true,
-
      "requires": {
-
        "asn1": "~0.2.3",
-
        "assert-plus": "^1.0.0",
-
        "bcrypt-pbkdf": "^1.0.0",
-
        "dashdash": "^1.12.0",
-
        "ecc-jsbn": "~0.1.1",
-
        "getpass": "^0.1.1",
-
        "jsbn": "~0.1.0",
-
        "safer-buffer": "^2.0.2",
-
        "tweetnacl": "~0.14.0"
-
      },
-
      "dependencies": {
-
        "tweetnacl": {
-
          "version": "0.14.5",
-
          "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-
          "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-
          "dev": true
-
        }
-
      }
-
    },
    "strict-uri-encode": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -12787,17 +10829,6 @@
        "safe-buffer": "~5.2.0"
      }
    },
-
    "string-width": {
-
      "version": "4.2.3",
-
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-
      "dev": true,
-
      "requires": {
-
        "emoji-regex": "^8.0.0",
-
        "is-fullwidth-code-point": "^3.0.0",
-
        "strip-ansi": "^6.0.1"
-
      }
-
    },
    "string.prototype.trimend": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
@@ -12832,12 +10863,6 @@
      "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
      "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="
    },
-
    "strip-final-newline": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-
      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-
      "dev": true
-
    },
    "strip-hex-prefix": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz",
@@ -12870,15 +10895,6 @@
        "acorn": "^8.8.0"
      }
    },
-
    "supports-color": {
-
      "version": "8.1.1",
-
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-
      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-
      "dev": true,
-
      "requires": {
-
        "has-flag": "^4.0.0"
-
      }
-
    },
    "supports-preserve-symlinks-flag": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -13005,18 +11021,6 @@
        }
      }
    },
-
    "throttleit": {
-
      "version": "1.0.0",
-
      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
-
      "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
-
      "dev": true
-
    },
-
    "through": {
-
      "version": "2.3.8",
-
      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-
      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
-
      "dev": true
-
    },
    "tinybench": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz",
@@ -13035,15 +11039,6 @@
      "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==",
      "dev": true
    },
-
    "tmp": {
-
      "version": "0.2.1",
-
      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
-
      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
-
      "dev": true,
-
      "requires": {
-
        "rimraf": "^3.0.0"
-
      }
-
    },
    "to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -13053,16 +11048,6 @@
        "is-number": "^7.0.0"
      }
    },
-
    "tough-cookie": {
-
      "version": "2.5.0",
-
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-
      "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-
      "dev": true,
-
      "requires": {
-
        "psl": "^1.1.28",
-
        "punycode": "^2.1.1"
-
      }
-
    },
    "tr46": {
      "version": "0.0.3",
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -13072,9 +11057,9 @@
      "peer": true
    },
    "tslib": {
-
      "version": "2.4.0",
-
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
-
      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+
      "version": "2.4.1",
+
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+
      "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
      "dev": true
    },
    "tsutils": {
@@ -13094,15 +11079,6 @@
        }
      }
    },
-
    "tunnel-agent": {
-
      "version": "0.6.0",
-
      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-
      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-
      "dev": true,
-
      "requires": {
-
        "safe-buffer": "^5.0.1"
-
      }
-
    },
    "tweetnacl": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
@@ -13221,18 +11197,6 @@
        "which-boxed-primitive": "^1.0.2"
      }
    },
-
    "universalify": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-
      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-
      "dev": true
-
    },
-
    "untildify": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
-
      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
-
      "dev": true
-
    },
    "uri-js": {
      "version": "4.4.1",
      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -13270,28 +11234,11 @@
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
    },
-
    "uuid": {
-
      "version": "8.3.2",
-
      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-
      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-
      "dev": true
-
    },
    "valid-url": {
      "version": "1.0.9",
      "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
      "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="
    },
-
    "verror": {
-
      "version": "1.10.0",
-
      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-
      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-
      "dev": true,
-
      "requires": {
-
        "assert-plus": "^1.0.0",
-
        "core-util-is": "1.0.2",
-
        "extsprintf": "^1.2.0"
-
      }
-
    },
    "vite": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz",
@@ -13315,9 +11262,9 @@
      }
    },
    "vitefu": {
-
      "version": "0.2.1",
-
      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.1.tgz",
-
      "integrity": "sha512-clkvXTAeUf+XQKm3bhWUhT4pye+3acm6YCTGaWhxxIvZZ/QjnA3JA8Zud+z/mO5y5XYvJJhevs5Sjkv/FI8nRw==",
+
      "version": "0.2.2",
+
      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.2.tgz",
+
      "integrity": "sha512-8CKEIWPm4B4DUDN+h+hVJa9pyNi7rzc5MYmbxhs1wcMakueGFNWB5/DL30USm9qU3xUPnL4/rrLEAwwFiD1tag==",
      "dev": true,
      "requires": {}
    },
@@ -13443,17 +11390,6 @@
      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
      "dev": true
    },
-
    "wrap-ansi": {
-
      "version": "7.0.0",
-
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-
      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-
      "dev": true,
-
      "requires": {
-
        "ansi-styles": "^4.0.0",
-
        "string-width": "^4.1.0",
-
        "strip-ansi": "^6.0.0"
-
      }
-
    },
    "wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -13487,16 +11423,6 @@
      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
      "dev": true
    },
-
    "yauzl": {
-
      "version": "2.10.0",
-
      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
-
      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
-
      "dev": true,
-
      "requires": {
-
        "buffer-crc32": "~0.2.3",
-
        "fd-slicer": "~1.1.0"
-
      }
-
    },
    "yocto-queue": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
modified package.json
@@ -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",
added playwright.buildSmoke.config.ts
@@ -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;
added playwright.config.ts
@@ -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;
added scripts/create-seed-fixture
@@ -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 .
added scripts/run-http-api-with-fixtures
@@ -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
deleted scripts/unit-test
@@ -1,5 +0,0 @@
-
#!/bin/sh
-
set -e
-

-
npm run test:unit
-
npm run test:components
modified src/App.svelte
@@ -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) {
deleted src/BlockTimer.spec.ts
@@ -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%;");
-
    });
-
  });
-
});
deleted src/ErrorModal.spec.ts
@@ -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");
-
  });
-
});
modified src/Header.svelte
@@ -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>
deleted src/NotFound.spec.ts
@@ -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");
-
  });
-
});
modified src/ReactionSelector.svelte
@@ -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;

modified src/Search.svelte
@@ -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 !== ""}
deleted src/SeedAddress.spec.ts
@@ -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");
-
  });
-
});
modified src/base/home/Index.svelte
@@ -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";

deleted src/base/projects/BranchSelector.spec.ts
@@ -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");
-
  });
-
});
modified src/base/projects/BranchSelector.svelte
@@ -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}
modified src/base/projects/Browser.svelte
@@ -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">
deleted src/base/projects/PeerSelector.spec.ts
@@ -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");
-
  });
-
});
modified src/base/projects/PeerSelector.svelte
@@ -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}
modified src/base/projects/View.svelte
@@ -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`.
modified src/base/registrations/registrar.ts
@@ -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,
deleted src/cache.test.ts
@@ -1,13 +0,0 @@
-
import { cached } from "@app/cache";
-
import { expect, test, vi } from "vitest";
-

-
test("it caches undefined return values", async () => {
-
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-
  const inner = vi.fn(async (_: string) => undefined);
-
  const memoized = cached(inner, key => key);
-

-
  expect(await memoized("a")).toBe(undefined);
-
  expect(await memoized("a")).toBe(undefined);
-

-
  expect(inner).toHaveBeenCalledTimes(1);
-
});
added src/config.ts
@@ -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();
added src/e2eTestStubs.ts
@@ -0,0 +1,8 @@
+
import * as FakeTimers from "@sinonjs/fake-timers";
+

+
if (typeof window.initializeTestStubs === "function") {
+
  window.e2eTestStubs = {
+
    FakeTimers: FakeTimers,
+
  };
+
  window.initializeTestStubs();
+
}
added src/global.d.ts
@@ -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 {};
modified src/index.ts
@@ -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;
modified src/router/index.ts
@@ -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("/");

modified src/session.ts
@@ -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,
deleted src/utils.test.ts
@@ -1,316 +0,0 @@
-
import type { Wallet } from "@app/wallet";
-

-
import { BigNumber } from "ethers";
-
import { describe, expect, test } from "vitest";
-
import * as utils from "./utils";
-

-
describe("Conversions", () => {
-
  test("toWei", () => {
-
    expect(utils.toWei("10")).toEqual(BigNumber.from("10000000000000000000"));
-
  });
-
});
-

-
describe("Format functions", () => {
-
  test.each([
-
    { amount: "1000", digits: 2, expected: "10.0" },
-
    { amount: "10000000000000000000", expected: "10.0" },
-
  ])("formatBalance", ({ amount, digits, expected }) => {
-
    expect(utils.formatBalance(BigNumber.from(amount), digits)).toEqual(
-
      expected,
-
    );
-
  });
-

-
  test.each([
-
    { hash: "#L42", expected: 42 },
-
    { hash: "#ETH", expected: null },
-
  ])("formatLocationHash $hash => $expected", ({ hash, expected }) => {
-
    expect(utils.formatLocationHash(hash)).toEqual(expected);
-
  });
-

-
  test.each([
-
    {
-
      id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
-
      expected: "hydkkk…coygh1",
-
    },
-
  ])("formatSeedId $id => $expected", ({ id, expected }) => {
-
    expect(utils.formatSeedId(id)).toEqual(expected);
-
  });
-

-
  test("formatRadicleUrn", () => {
-
    expect(
-
      utils.formatRadicleUrn("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
-
    ).toEqual("rad:git:hnrkem…d84c9o");
-
  });
-

-
  test("formatRadicleUrn throw when wrong URN", () => {
-
    expect(() =>
-
      utils.formatRadicleUrn("hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
-
    ).toThrow();
-
  });
-

-
  test("formatAddress", () => {
-
    expect(
-
      utils.formatAddress("0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511"),
-
    ).toEqual("b5d8 – F511");
-

-
    expect(() => utils.formatAddress("0x8f91813")).toThrowError(
-
      'invalid address (argument="address", value="0x8f91813", code=INVALID_ARGUMENT, version=address/5.7.0)',
-
    );
-
  });
-

-
  test.each([
-
    {
-
      input: "seedling",
-
      expected: "🌱",
-
    },
-
    {
-
      input: "+1",
-
      expected: "👍",
-
    },
-
    {
-
      input: "radicle",
-
      expected: "radicle",
-
    },
-
  ])("parseEmoji $input => $expected", ({ input, expected }) => {
-
    expect(utils.parseEmoji(input)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { commit: "a8a6a979a6261a2ec1ea85fc9a65a4a30aa22cc8", expected: "a8a6a97" },
-
    { commit: "a8a6a97", expected: "a8a6a97" },
-
  ])("formatCommit $commit => $expected", ({ commit, expected }) => {
-
    expect(utils.formatCommit(commit)).toEqual(expected);
-
  });
-
});
-

-
describe("String Assertions", () => {
-
  test.each([
-
    {
-
      a: "0x1234567890123456789012345678901234567890",
-
      b: "0x1234567890123456789012345678901234567890",
-
      expected: true,
-
    },
-
    {
-
      a: "0x1234567890123456789012345678901234567890",
-
      b: "0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
-
      expected: false,
-
    },
-
  ])("isAddressEqual", ({ a, b, expected }) => {
-
    expect(utils.isAddressEqual(a, b)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { domain: "alt-clients.radicle.xyz", expected: true },
-
    { domain: "0.0.0.0", expected: true }, // Pass as true since we are not in production
-
    { domain: "", expected: false },
-
  ])("isDomain $domain => $expected", ({ domain, expected }) => {
-
    expect(utils.isDomain(domain)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { path: "README.md", expected: true },
-
    { path: "README.mkd", expected: true },
-
    { path: "README.markdown", expected: true },
-
    { path: "", expected: false },
-
  ])("isMarkdownPath $path => $expected", ({ path, expected }) => {
-
    expect(utils.isMarkdownPath(path)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
-
    { id: "0x1234567890123456789012345678901234567890", expected: false },
-
  ])("isRadicleId $id => $expected", ({ id, expected }) => {
-
    expect(utils.isRadicleId(id)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { id: "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio", expected: true },
-
    { id: "0x1234567890123456789012345678901234567890", expected: false },
-
  ])("isPeerId $id => $expected", ({ id, expected }) => {
-
    expect(utils.isPeerId(id)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { oid: "a64ae9c6d572e0ad906faa9a4a7a8d43f113278c", expected: true },
-
    { oid: "a64ae9c", expected: false },
-
  ])("isOid $oid => $expected", ({ oid, expected }) => {
-
    expect(utils.isOid(oid)).toEqual(expected);
-
  });
-

-
  test.each([
-
    { address: "0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0", expected: true },
-
    { address: "0x5E813e48a81977c6fdd565ed5097eb600c73c4f0", expected: false }, // If address is badly checksummed => false
-
    { address: "0x5e813e48a81977c6fdd565ed5097eb600c73c4f0", expected: true },
-
  ])("isAddress $address => $expected", ({ address, expected }) => {
-
    expect(utils.isAddress(address)).toBe(expected);
-
  });
-

-
  test.each([
-
    { url: "https://app.radicle.xyz", expected: true },
-
    { url: "http://app.radicle.xyz", expected: true },
-
    { url: "http://app", expected: true },
-
    { url: "://app", expected: false },
-
    { url: "//app", expected: false },
-
    { url: "app", expected: false },
-
  ])("isUrl $url => $expected", ({ url, expected }) => {
-
    expect(utils.isUrl(url)).toBe(expected);
-
  });
-
});
-

-
describe("Others", () => {
-
  test.each([
-
    {
-
      name: "goerli",
-
      expected:
-
        "https://goerli.etherscan.io/address/0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
-
    },
-
    {
-
      name: "",
-
      expected:
-
        "https://etherscan.io/address/0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
-
    },
-
  ])("explorerLink $name => $expected", ({ name, expected }) => {
-
    expect(
-
      utils.explorerLink("0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0", {
-
        network: {
-
          name,
-
        },
-
      } as Wallet),
-
    ).toEqual(expected);
-
  });
-
});
-

-
describe("Parse Strings", () => {
-
  test.each([
-
    { label: "sebastinez.radicle.eth", expected: "sebastinez" },
-
    { label: "sebastinez", expected: "sebastinez" },
-
  ])("parseEnsLabel", ({ label, expected }) => {
-
    expect(
-
      utils.parseEnsLabel(label, {
-
        registrar: {
-
          address: "0x1234567890123456789012345678901234567890",
-
          domain: "radicle.eth",
-
        },
-
      } as Wallet),
-
    ).toEqual(expected);
-
  });
-

-
  test.each([
-
    { input: "https://twitter.com/cloudhead", expected: "cloudhead" },
-
    { input: "sebastinez", expected: "sebastinez" },
-
  ])("parseUsername", ({ input, expected }) => {
-
    expect(utils.parseUsername(input)).toEqual(expected);
-
  });
-
});
-

-
describe("Path Manipulation", () => {
-
  test.each([
-
    {
-
      imagePath: "/assets/images/tux.png",
-
      base: "/",
-
      origin: "https://app.radicle.xyz",
-
      expected: "assets/images/tux.png",
-
    },
-
    {
-
      imagePath: "assets/images/tux.png",
-
      base: "/",
-
      origin: "https://app.radicle.xyz",
-
      expected: "assets/images/tux.png",
-
    },
-
    {
-
      imagePath: "assets/images/tux.png",
-
      base: "/",
-
      origin: "http://localhost:3000",
-
      expected: "assets/images/tux.png",
-
    },
-
    {
-
      imagePath: "../tux.png",
-
      base: "/components/assets/README.md",
-
      origin: "http://localhost:3000",
-
      expected: "components/tux.png",
-
    },
-
    {
-
      imagePath: "../tux.png",
-
      base: "/components/assets/",
-
      origin: "http://localhost:3000",
-
      expected: "components/tux.png",
-
    },
-
    {
-
      imagePath: "../../tux.png",
-
      base: "/components/assets/images/README.md",
-
      origin: "http://localhost:3000",
-
      expected: "components/tux.png",
-
    },
-
  ])(
-
    "canonicalize origin: $origin base: $base, path: $imagePath => $expected",
-
    ({ imagePath, base, expected, origin }) => {
-
      expect(utils.canonicalize(imagePath, base, origin)).toEqual(expected);
-
    },
-
  );
-
});
-

-
describe("Date Manipulation", () => {
-
  test.each([
-
    { from: new Date("2022-01-01"), to: new Date("2022-02-01"), expected: 31 },
-
    { from: new Date("2022-01-01"), to: new Date("2022-01-02"), expected: 1 },
-
    { from: new Date("2022-01-01"), to: new Date("2022-01-01"), expected: 0 },
-
  ])("getDaysPassed expected: $expected ", ({ from, to, expected }) => {
-
    expect(utils.getDaysPassed(from, to)).toEqual(expected);
-
  });
-
  test.each([
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-01 12:00:00"),
-
      expected: "now",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-01 12:00:01"),
-
      expected: "1 second ago",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-01 12:01:01"),
-
      expected: "1 minute ago",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-01 13:01:01"),
-
      expected: "1 hour ago",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-02 13:01:01"),
-
      expected: "yesterday",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-01-04 13:01:01"),
-
      expected: "3 days ago",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-02-02 13:01:01"),
-
      expected: "last month",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2022-04-02 13:01:01"),
-
      expected: "3 months ago",
-
    },
-
    {
-
      from: new Date("2022-01-01 12:00:00"),
-
      to: new Date("2023-04-02 12:00:00"),
-
      expected: "Sat, 01 Jan 2022 12:00:00 GMT",
-
    },
-
    {
-
      from: new Date("2022-03-05 12:00:00"),
-
      to: new Date("2026-04-02 12:00:00"),
-
      expected: "Sat, 05 Mar 2022 12:00:00 GMT",
-
    },
-
  ])("formatTimestamp expected: $expected", ({ from, to, expected }) => {
-
    expect(utils.formatTimestamp(from.getTime() / 1000, to.getTime())).toEqual(
-
      expected,
-
    );
-
  });
-
});
modified src/utils.ts
@@ -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 {
modified src/wallet.ts
@@ -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`.
added tests/e2e/buildSmoke.spec.ts
@@ -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();
+
});
added tests/e2e/clipboard.spec.ts
@@ -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('')");
+
});
added tests/e2e/landingPage.spec.ts
@@ -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();
+
});
added tests/e2e/project.spec.ts
@@ -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();
+
});
added tests/e2e/project/commit.spec.ts
@@ -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();
+
});
added tests/e2e/project/commits.spec.ts
@@ -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",
+
  );
+
});
added tests/e2e/router.hash.spec.ts
@@ -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);
+
  });
+
});
added tests/e2e/router.history.spec.ts
@@ -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);
+
  });
+
});
added tests/e2e/search.spec.ts
@@ -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",
+
  );
+
});
added tests/e2e/seed.spec.ts
@@ -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();
+
  }
+
});
added tests/e2e/settings.spec.ts
@@ -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",
+
  );
+
});
added tests/fixtures/repos/source-browsing.tar.bz2
added tests/fixtures/seeds/palm.tar.bz2
added tests/support/fixtures.ts
@@ -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);
+
  }
+
}
added tests/support/globalSetup.ts
@@ -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);
+
  }
+
}
added tests/support/logLabel.ts
@@ -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)} | `,
+
  );
+
}
added tests/support/router.ts
@@ -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);
+
};
added tests/tmp/.gitkeep
added tests/unit/cache.test.ts
@@ -0,0 +1,13 @@
+
import { cached } from "@app/cache";
+
import { expect, test, vi } from "vitest";
+

+
test("it caches undefined return values", async () => {
+
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+
  const inner = vi.fn(async (_: string) => undefined);
+
  const memoized = cached(inner, key => key);
+

+
  expect(await memoized("a")).toBe(undefined);
+
  expect(await memoized("a")).toBe(undefined);
+

+
  expect(inner).toHaveBeenCalledTimes(1);
+
});
added tests/unit/utils.test.ts
@@ -0,0 +1,316 @@
+
import type { Wallet } from "@app/wallet";
+

+
import { BigNumber } from "ethers";
+
import { describe, expect, test } from "vitest";
+
import * as utils from "@app/utils";
+

+
describe("Conversions", () => {
+
  test("toWei", () => {
+
    expect(utils.toWei("10")).toEqual(BigNumber.from("10000000000000000000"));
+
  });
+
});
+

+
describe("Format functions", () => {
+
  test.each([
+
    { amount: "1000", digits: 2, expected: "10.0" },
+
    { amount: "10000000000000000000", expected: "10.0" },
+
  ])("formatBalance", ({ amount, digits, expected }) => {
+
    expect(utils.formatBalance(BigNumber.from(amount), digits)).toEqual(
+
      expected,
+
    );
+
  });
+

+
  test.each([
+
    { hash: "#L42", expected: 42 },
+
    { hash: "#ETH", expected: null },
+
  ])("formatLocationHash $hash => $expected", ({ hash, expected }) => {
+
    expect(utils.formatLocationHash(hash)).toEqual(expected);
+
  });
+

+
  test.each([
+
    {
+
      id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
+
      expected: "hydkkk…coygh1",
+
    },
+
  ])("formatSeedId $id => $expected", ({ id, expected }) => {
+
    expect(utils.formatSeedId(id)).toEqual(expected);
+
  });
+

+
  test("formatRadicleUrn", () => {
+
    expect(
+
      utils.formatRadicleUrn("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
+
    ).toEqual("rad:git:hnrkem…d84c9o");
+
  });
+

+
  test("formatRadicleUrn throw when wrong URN", () => {
+
    expect(() =>
+
      utils.formatRadicleUrn("hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
+
    ).toThrow();
+
  });
+

+
  test("formatAddress", () => {
+
    expect(
+
      utils.formatAddress("0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511"),
+
    ).toEqual("b5d8 – F511");
+

+
    expect(() => utils.formatAddress("0x8f91813")).toThrowError(
+
      'invalid address (argument="address", value="0x8f91813", code=INVALID_ARGUMENT, version=address/5.7.0)',
+
    );
+
  });
+

+
  test.each([
+
    {
+
      input: "seedling",
+
      expected: "🌱",
+
    },
+
    {
+
      input: "+1",
+
      expected: "👍",
+
    },
+
    {
+
      input: "radicle",
+
      expected: "radicle",
+
    },
+
  ])("parseEmoji $input => $expected", ({ input, expected }) => {
+
    expect(utils.parseEmoji(input)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { commit: "a8a6a979a6261a2ec1ea85fc9a65a4a30aa22cc8", expected: "a8a6a97" },
+
    { commit: "a8a6a97", expected: "a8a6a97" },
+
  ])("formatCommit $commit => $expected", ({ commit, expected }) => {
+
    expect(utils.formatCommit(commit)).toEqual(expected);
+
  });
+
});
+

+
describe("String Assertions", () => {
+
  test.each([
+
    {
+
      a: "0x1234567890123456789012345678901234567890",
+
      b: "0x1234567890123456789012345678901234567890",
+
      expected: true,
+
    },
+
    {
+
      a: "0x1234567890123456789012345678901234567890",
+
      b: "0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
+
      expected: false,
+
    },
+
  ])("isAddressEqual", ({ a, b, expected }) => {
+
    expect(utils.isAddressEqual(a, b)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { domain: "alt-clients.radicle.xyz", expected: true },
+
    { domain: "0.0.0.0", expected: true }, // Pass as true since we are not in production
+
    { domain: "", expected: false },
+
  ])("isDomain $domain => $expected", ({ domain, expected }) => {
+
    expect(utils.isDomain(domain)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { path: "README.md", expected: true },
+
    { path: "README.mkd", expected: true },
+
    { path: "README.markdown", expected: true },
+
    { path: "", expected: false },
+
  ])("isMarkdownPath $path => $expected", ({ path, expected }) => {
+
    expect(utils.isMarkdownPath(path)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
+
    { id: "0x1234567890123456789012345678901234567890", expected: false },
+
  ])("isRadicleId $id => $expected", ({ id, expected }) => {
+
    expect(utils.isRadicleId(id)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { id: "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio", expected: true },
+
    { id: "0x1234567890123456789012345678901234567890", expected: false },
+
  ])("isPeerId $id => $expected", ({ id, expected }) => {
+
    expect(utils.isPeerId(id)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { oid: "a64ae9c6d572e0ad906faa9a4a7a8d43f113278c", expected: true },
+
    { oid: "a64ae9c", expected: false },
+
  ])("isOid $oid => $expected", ({ oid, expected }) => {
+
    expect(utils.isOid(oid)).toEqual(expected);
+
  });
+

+
  test.each([
+
    { address: "0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0", expected: true },
+
    { address: "0x5E813e48a81977c6fdd565ed5097eb600c73c4f0", expected: false }, // If address is badly checksummed => false
+
    { address: "0x5e813e48a81977c6fdd565ed5097eb600c73c4f0", expected: true },
+
  ])("isAddress $address => $expected", ({ address, expected }) => {
+
    expect(utils.isAddress(address)).toBe(expected);
+
  });
+

+
  test.each([
+
    { url: "https://app.radicle.xyz", expected: true },
+
    { url: "http://app.radicle.xyz", expected: true },
+
    { url: "http://app", expected: true },
+
    { url: "://app", expected: false },
+
    { url: "//app", expected: false },
+
    { url: "app", expected: false },
+
  ])("isUrl $url => $expected", ({ url, expected }) => {
+
    expect(utils.isUrl(url)).toBe(expected);
+
  });
+
});
+

+
describe("Others", () => {
+
  test.each([
+
    {
+
      name: "goerli",
+
      expected:
+
        "https://goerli.etherscan.io/address/0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
+
    },
+
    {
+
      name: "",
+
      expected:
+
        "https://etherscan.io/address/0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0",
+
    },
+
  ])("explorerLink $name => $expected", ({ name, expected }) => {
+
    expect(
+
      utils.explorerLink("0x5E813e48a81977c6Fdd565ed5097eb600C73C4f0", {
+
        network: {
+
          name,
+
        },
+
      } as Wallet),
+
    ).toEqual(expected);
+
  });
+
});
+

+
describe("Parse Strings", () => {
+
  test.each([
+
    { label: "sebastinez.radicle.eth", expected: "sebastinez" },
+
    { label: "sebastinez", expected: "sebastinez" },
+
  ])("parseEnsLabel", ({ label, expected }) => {
+
    expect(
+
      utils.parseEnsLabel(label, {
+
        registrar: {
+
          address: "0x1234567890123456789012345678901234567890",
+
          domain: "radicle.eth",
+
        },
+
      } as Wallet),
+
    ).toEqual(expected);
+
  });
+

+
  test.each([
+
    { input: "https://twitter.com/cloudhead", expected: "cloudhead" },
+
    { input: "sebastinez", expected: "sebastinez" },
+
  ])("parseUsername", ({ input, expected }) => {
+
    expect(utils.parseUsername(input)).toEqual(expected);
+
  });
+
});
+

+
describe("Path Manipulation", () => {
+
  test.each([
+
    {
+
      imagePath: "/assets/images/tux.png",
+
      base: "/",
+
      origin: "https://app.radicle.xyz",
+
      expected: "assets/images/tux.png",
+
    },
+
    {
+
      imagePath: "assets/images/tux.png",
+
      base: "/",
+
      origin: "https://app.radicle.xyz",
+
      expected: "assets/images/tux.png",
+
    },
+
    {
+
      imagePath: "assets/images/tux.png",
+
      base: "/",
+
      origin: "http://localhost:3000",
+
      expected: "assets/images/tux.png",
+
    },
+
    {
+
      imagePath: "../tux.png",
+
      base: "/components/assets/README.md",
+
      origin: "http://localhost:3000",
+
      expected: "components/tux.png",
+
    },
+
    {
+
      imagePath: "../tux.png",
+
      base: "/components/assets/",
+
      origin: "http://localhost:3000",
+
      expected: "components/tux.png",
+
    },
+
    {
+
      imagePath: "../../tux.png",
+
      base: "/components/assets/images/README.md",
+
      origin: "http://localhost:3000",
+
      expected: "components/tux.png",
+
    },
+
  ])(
+
    "canonicalize origin: $origin base: $base, path: $imagePath => $expected",
+
    ({ imagePath, base, expected, origin }) => {
+
      expect(utils.canonicalize(imagePath, base, origin)).toEqual(expected);
+
    },
+
  );
+
});
+

+
describe("Date Manipulation", () => {
+
  test.each([
+
    { from: new Date("2022-01-01"), to: new Date("2022-02-01"), expected: 31 },
+
    { from: new Date("2022-01-01"), to: new Date("2022-01-02"), expected: 1 },
+
    { from: new Date("2022-01-01"), to: new Date("2022-01-01"), expected: 0 },
+
  ])("getDaysPassed expected: $expected ", ({ from, to, expected }) => {
+
    expect(utils.getDaysPassed(from, to)).toEqual(expected);
+
  });
+
  test.each([
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-01 12:00:00"),
+
      expected: "now",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-01 12:00:01"),
+
      expected: "1 second ago",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-01 12:01:01"),
+
      expected: "1 minute ago",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-01 13:01:01"),
+
      expected: "1 hour ago",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-02 13:01:01"),
+
      expected: "yesterday",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-01-04 13:01:01"),
+
      expected: "3 days ago",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-02-02 13:01:01"),
+
      expected: "last month",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2022-04-02 13:01:01"),
+
      expected: "3 months ago",
+
    },
+
    {
+
      from: new Date("2022-01-01 12:00:00"),
+
      to: new Date("2023-04-02 12:00:00"),
+
      expected: "Sat, 01 Jan 2022 12:00:00 GMT",
+
    },
+
    {
+
      from: new Date("2022-03-05 12:00:00"),
+
      to: new Date("2026-04-02 12:00:00"),
+
      expected: "Sat, 05 Mar 2022 12:00:00 GMT",
+
    },
+
  ])("formatTimestamp expected: $expected", ({ from, to, expected }) => {
+
    expect(utils.formatTimestamp(from.getTime() / 1000, to.getTime())).toEqual(
+
      expected,
+
    );
+
  });
+
});
modified tsconfig.json
@@ -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
modified vite.config.ts
@@ -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(),
+
});