diff --git a/.github/workflows/check-e2e.yml b/.github/workflows/check-e2e.yml index 36072c27b..5a876c68b 100644 --- a/.github/workflows/check-e2e.yml +++ b/.github/workflows/check-e2e.yml @@ -12,7 +12,7 @@ jobs: continue-on-error: true strategy: matrix: - browser: [chromium, firefox, visual] + browser: [chromium, firefox, visual, heartwood] timeout-minutes: 30 runs-on: ubuntu-latest steps: @@ -59,12 +59,19 @@ jobs: run: npx playwright install-deps - name: Start http-api test server - run: ./scripts/run-http-api-with-fixtures --non-interactive --detach + run: | + if [ ${{ matrix.browser }} = "heartwood" ]; then + ./scripts/run-httpd-with-fixtures --non-interactive --detach + else + ./scripts/run-http-api-with-fixtures --non-interactive --detach + fi - name: Run Playwright tests run: | if [ ${{ matrix.browser }} = "visual" ] && [ ${{ github.ref }} = "refs/heads/master" ]; then npm run test:e2e -- --project ${{ matrix.browser }} --update-snapshots; + elif [ ${{ matrix.browser }} = "heartwood" ]; then + HEARTWOOD=true npm run test:e2e -- --project chromium; else npm run test:e2e -- --project ${{ matrix.browser }}; fi diff --git a/scripts/create-seed-fixture-heartwood b/scripts/create-seed-fixture-heartwood new file mode 100755 index 000000000..894425184 --- /dev/null +++ b/scripts/create-seed-fixture-heartwood @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +PASSPHRASE=asdf +SEED="0.0.0.0:8080" + +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_PASSPHRASE=$PASSPHRASE rad auth + +GIT_AUTHOR_NAME="Alice Liddell" +GIT_AUTHOR_EMAIL="alice@radicle.xyz" +GIT_COMMITTER_NAME="Alice Liddell" +GIT_COMMITTER_EMAIL="alice@radicle.xyz" + +RAD_DEBUG=1 RAD_HOME=$ALICE_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE rad auth + +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_PASSPHRASE=$PASSPHRASE rad init --name "source-browsing" --description "Git repository for source browsing tests" --default-branch "main" --no-confirm +PROJECT_ID=$(RAD_HOME=$ALICE_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE rad .) + +cd $BASE_PATH +tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $ALICE_RAD_HOME . diff --git a/scripts/run-http-api-with-fixtures b/scripts/run-http-api-with-fixtures index d3947d4de..461d2fff1 100755 --- a/scripts/run-http-api-with-fixtures +++ b/scripts/run-http-api-with-fixtures @@ -10,8 +10,7 @@ PASSPHRASE=asdf CONTAINER_NAME=radicle-http-api-with-fixtures HTTP_API_BINARY=radicle-http-api -show_usage() -{ +show_usage() { echo echo "Starts a http-api backend with test fixtures." echo @@ -26,8 +25,7 @@ show_usage() echo } -prompt_workspace_removal() -{ +prompt_workspace_removal() { echo "This will irrevocably destroy the following directories:" echo echo $WORKSPACE @@ -35,19 +33,18 @@ prompt_workspace_removal() 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 - ;; + [yY][eE][sS]) + rm -rf $WORKSPACE + echo "Done" + ;; + *) + echo "Ok, I won't touch your data." + exit + ;; esac } -prepare_workspace() -{ +prepare_workspace() { echo echo "Unpacking fixture $FIXTURE" mkdir -p $WORKSPACE @@ -94,13 +91,13 @@ DETACH=false while [ $# -ne 0 ]; do case "$1" in - --binary|-b) + --binary | -b) BINARY=true ;; - --detach|-d) + --detach | -d) DETACH=true ;; - --non-interactive|-n) + --non-interactive | -n) NON_INTERACTIVE=true ;; *) @@ -120,7 +117,6 @@ else prepare_workspace fi - if [ "$BINARY" = true ]; then run_binary else diff --git a/scripts/run-httpd-with-fixtures b/scripts/run-httpd-with-fixtures new file mode 100755 index 000000000..78618c30d --- /dev/null +++ b/scripts/run-httpd-with-fixtures @@ -0,0 +1,132 @@ +#!/bin/sh +set -e + +REV=1f55d7a32750b3e63c56aad00370411b90420a29 + +REPO_ROOT=$(git rev-parse --show-toplevel) +FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm-heartwood.tar.bz2 +WORKSPACE=$REPO_ROOT/tests/tmp/palm +PASSPHRASE=asdf +CONTAINER_NAME=radicle-httpd-with-fixtures +HTTP_API_BINARY=radicle-httpd + +show_usage() { + echo + echo "Starts a radicle-httpd backend with test fixtures." + echo + echo "USAGE:" + echo " run-httpd-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 + if [ "$CI" = true ]; then + sudo chown -R 0:0 $WORKSPACE + fi +} + +run_docker() { + echo "Starting docker at container $CONTAINER_NAME at $REV" + echo " RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE radicle-http" + echo + + exec docker run \ + --init \ + --publish 8080:8080 \ + --rm \ + --name $CONTAINER_NAME \ + --volume $WORKSPACE:/app/radicle \ + "$@" \ + --env "RAD_HOME=/app/radicle" \ + --env "RAD_PASSPHRASE=$PASSPHRASE" \ + "gcr.io/radicle-services/radicle-httpd:$REV" +} + +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/heartwood" + echo + exit 1 + fi + + echo + echo "Starting $HTTP_API_BINARY" + echo " RAD_HOME=$WORKSPACE $HTTP_API_BINARY" + echo + + RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE $HTTP_API_BINARY +} + +BINARY=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 diff --git a/src/components/Authorship.svelte b/src/components/Authorship.svelte index 70cf25360..a3d3af5d6 100644 --- a/src/components/Authorship.svelte +++ b/src/components/Authorship.svelte @@ -6,7 +6,11 @@ import Address from "@app/components/Address.svelte"; import { Profile, ProfileType } from "@app/lib/profile"; - import { formatRadicleId, formatTimestamp } from "@app/lib/utils"; + import { + formatRadicleId, + formatSeedId, + formatTimestamp, + } from "@app/lib/utils"; export let noAvatar = false; export let author: Author; @@ -67,7 +71,7 @@ {:else} - {formatRadicleId(author.id)} + {window.HEARTWOOD ? formatSeedId(author.id) : formatRadicleId(author.id)} {/if}  {caption}  diff --git a/src/components/Form.svelte b/src/components/Form.svelte index 35188d436..c402c498c 100644 --- a/src/components/Form.svelte +++ b/src/components/Form.svelte @@ -20,17 +20,19 @@ const validationExamples: Record = { URL: "https://acme.xyz/", - URN: "eip155:1:0xd1bb21bd5a432d2919c82bcefe1bc7f8cc9207d9", + ID: "eip155:1:0xd1bb21bd5a432d2919c82bcefe1bc7f8cc9207d9", handle: "acme", id: "hydkkcf6k9be5fuszdhpqbctu3q3fuwagj874wx2puia8ti8coygh1", - identity: "rad:git:hnrkqdpm9ub19oc8dccx44echy75hzfsezyio", + identity: window.HEARTWOOD + ? "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT" + : "rad:git:hnrkqdpm9ub19oc8dccx44echy75hzfsezyio", domain: "seed.acme.xyz", address: "0x17a8c096733BD5F87aD43D7A2A4d1C42ab8A2A70", }; const validationTypes: { [index: string]: RegExp } = { URL: /^(https:\/\/|http:\/\/|ipfs:\/\/)\S+/, - URN: /^[a-z]+:[a-zA-Z0-9:-]{1,64}$/, + ID: /^z[a-z]+:[a-zA-Z0-9:-]{1,64}$/, // Github // Username may only contain alphanumeric characters or hyphens. // Username cannot have multiple consecutive hyphens. @@ -43,7 +45,9 @@ handle: /^[a-zA-Z0-9-_]{1,39}$/, address: /^0x[a-zA-Z0-9]{40}$/, id: /^[a-z0-9]+$/, - identity: /^rad:git:[a-z0-9]{37}$/, + identity: window.HEARTWOOD + ? /^rad:[a-z0-9]{28}$/ + : /^rad:git:[a-z0-9]{37}$/, domain: /^[^/:$!_;,@#]+\.[a-z]{2,}$/, }; diff --git a/src/global.d.ts b/src/global.d.ts index 744f264ac..67d3d6d48 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -10,6 +10,8 @@ declare global { PLAYWRIGHT: boolean; HASH_ROUTING: boolean; + HEARTWOOD: 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 diff --git a/src/lib/api.ts b/src/lib/api.ts index 3092ebf09..8a9207be2 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -13,7 +13,11 @@ export class Request { constructor(path: string, api: Host) { this.port = api.port || defaultSeedPort; - this.base = api.host; + if (window.HEARTWOOD) { + this.base = api.host + "/api"; + } else { + this.base = api.host; + } this.path = path.startsWith("/") ? path.slice(1) : path; this.protocol = api.host === "0.0.0.0" ? "http://" : "https://"; } diff --git a/src/lib/diff.ts b/src/lib/diff.ts index 086bbddf6..a2321f2ba 100644 --- a/src/lib/diff.ts +++ b/src/lib/diff.ts @@ -89,6 +89,7 @@ export interface Diff { moved: string[]; copied: string[]; modified: FileDiff[]; + stats: DiffStats; } export interface DiffStats { diff --git a/src/lib/issue.ts b/src/lib/issue.ts index 8e82765a6..25326afd1 100644 --- a/src/lib/issue.ts +++ b/src/lib/issue.ts @@ -12,7 +12,7 @@ export interface IIssue { author: Author; title: string; state: State; - comment: Comment; + comment: Comment; // TODO: Remove this after we have migrated to Heartwood. discussion: Thread[]; tags: Tag[]; timestamp: number; @@ -32,7 +32,8 @@ export interface Comment { body: string; reactions: Record; timestamp: number; - replies: R; + replies: R; // TODO: Remove this after we have migrated to Heartwood. + replyTo: R; } export type Thread = Comment; @@ -57,7 +58,7 @@ export class Issue { author: Author; title: string; state: State; - comment: Comment; + comment: Comment; // TODO: Remove this after we have migrated to Heartwood. discussion: Thread[]; tags: Tag[]; timestamp: number; @@ -67,18 +68,28 @@ export class Issue { this.author = issue.author; this.title = issue.title; this.state = issue.state; - this.comment = issue.comment; + this.comment = issue.comment; // TODO: Remove this after we have migrated to Heartwood. this.discussion = issue.discussion; this.tags = issue.tags; - this.timestamp = issue.timestamp; + if (window.HEARTWOOD) { + this.timestamp = issue.discussion[0].timestamp; + } else { + this.timestamp = issue.timestamp; + } } // Counts the amount of comments and replies in a discussion countComments(): number { - return this.discussion.reduce((acc, comment) => { - if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 forathe comment in this loop. - return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. - }, 0); + if (window.HEARTWOOD) { + return this.discussion.reduce(acc => { + return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. + }, 0); + } else { + return this.discussion.reduce((acc, comment) => { + if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 forathe comment in this loop. + return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. + }, 0); + } } static async getIssues(id: string, host: Host): Promise { diff --git a/src/lib/patch.ts b/src/lib/patch.ts index e3bd5da9c..c6a8fedff 100644 --- a/src/lib/patch.ts +++ b/src/lib/patch.ts @@ -100,10 +100,16 @@ export class Patch implements IPatch { // Counts the amount of comments and replies in a discussion countComments(rev: number): number { - return this.revisions[rev].discussion.reduce((acc, comment) => { - if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 for each comment in this loop. - return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. - }, 0); + if (window.HEARTWOOD) { + return this.revisions[rev].discussion.reduce(acc => { + return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. + }, 0); + } else { + return this.revisions[rev].discussion.reduce((acc, comment) => { + if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 for each comment in this loop. + return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop. + }, 0); + } } createTimeline(rev: number) { diff --git a/src/lib/project.ts b/src/lib/project.ts index a52b4f906..19914a2a4 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -12,6 +12,7 @@ export type Branches = { [key: string]: string }; export type MaybeBlob = Blob | undefined; export type MaybeTree = Tree | undefined; +// TODO: Remove this after we have migrated to Heartwood. export type Delegate = | { type: "indirect"; @@ -40,38 +41,43 @@ export interface ProjectInfo { name: string; description: string; defaultBranch: string; - delegates: Delegate[]; - remotes: PeerId[]; + delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood. + remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood. patches?: number; issues?: number; } export interface Tree { path: string; - info: EntryInfo; + info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood. entries: Array; stats: Stats; + name: string; + kind: Kind; + lastCommit: CommitHeader; } +// TODO: Remove "TREE" and "BLOB" after we have migrated to Heartwood. +type Kind = "tree" | "blob" | "TREE" | "BLOB"; + export interface Stats { commits: number; contributors: number; } -export enum ObjectType { - Blob = "BLOB", - Tree = "TREE", -} - +// TODO: Remove this after we have migrated to Heartwood. export interface EntryInfo { name: string; - objectType: ObjectType; + objectType: Kind; lastCommit: CommitHeader; } export interface Entry { path: string; - info: EntryInfo; + info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood. + name: string; + kind: Kind; + lastCommit: CommitHeader; } export interface Blob { @@ -79,7 +85,10 @@ export interface Blob { html?: boolean; content: string; path: string; - info: EntryInfo; + info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood. + name: string; + kind: Kind; + lastCommit: CommitHeader; } export interface Remote { @@ -144,8 +153,8 @@ export class Project implements ProjectInfo { name: string; description: string; defaultBranch: string; - delegates: Delegate[]; - remotes: PeerId[]; + delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood. + remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood. seed: Seed; peers: Peer[]; branches: Branches; @@ -167,8 +176,8 @@ export class Project implements ProjectInfo { this.name = info.name; this.description = info.description; this.defaultBranch = info.defaultBranch; - this.delegates = info.delegates; - this.remotes = info.remotes; + this.delegates = info.delegates; // TODO: Remove this after we have migrated to Heartwood. + this.remotes = info.remotes; // TODO: Remove this after we have migrated to Heartwood. this.seed = seed; this.peers = peers; this.branches = branches; @@ -192,10 +201,14 @@ export class Project implements ProjectInfo { } static async getInfo(nameOrId: string, host: Host): Promise { - const result = await new Request(`projects/${nameOrId}`, host).get(); - result["id"] = result["urn"]; - delete result["urn"]; - return result; + if (window.HEARTWOOD) { + return await new Request(`projects/${nameOrId}`, host).get(); + } else { + const result = await new Request(`projects/${nameOrId}`, host).get(); + result["id"] = result["urn"]; + delete result["urn"]; + return result; + } } static async getProjects( @@ -209,13 +222,17 @@ export class Project implements ProjectInfo { "per-page": opts?.perPage, page: opts?.page, }; - const results = await new Request("projects", host).get(params); - results.forEach((result: { [x: string]: any }) => { - result["id"] = result["urn"]; - delete result["urn"]; - }); + if (window.HEARTWOOD) { + return await new Request("projects", host).get(params); + } else { + const results = await new Request("projects", host).get(params); + results.forEach((result: { [x: string]: any }) => { + result["id"] = result["urn"]; + delete result["urn"]; + }); - return results; + return results; + } } static async getDelegateProjects( @@ -280,28 +297,31 @@ export class Project implements ProjectInfo { `projects/${this.id}/commits/${commit}`, this.seed.addr, ).get(); - result.stats["insertions"] = result.stats["additions"]; - delete result.stats["additions"]; - result.diff["added"] = result.diff["created"]; - delete result.diff["created"]; - for (const kind of ["added", "deleted", "modified"]) { - for (const file of result.diff[kind]) { - for (const hunk of file.diff.hunks) { - for (const line of hunk.lines) { - if (line["lineNumOld"]) { - line["lineNoOld"] = line["lineNumOld"]; - delete line["lineNumOld"]; - } + if (!window.HEARTWOOD) { + result.stats["insertions"] = result.stats["additions"]; + delete result.stats["additions"]; + result.diff["added"] = result.diff["created"]; + delete result.diff["created"]; - if (line["lineNumNew"]) { - line["lineNoNew"] = line["lineNumNew"]; - delete line["lineNumNew"]; - } + for (const kind of ["added", "deleted", "modified"]) { + for (const file of result.diff[kind]) { + for (const hunk of file.diff.hunks) { + for (const line of hunk.lines) { + if (line["lineNumOld"]) { + line["lineNoOld"] = line["lineNumOld"]; + delete line["lineNumOld"]; + } - if (line["lineNum"]) { - line["lineNo"] = line["lineNum"]; - delete line["lineNum"]; + if (line["lineNumNew"]) { + line["lineNoNew"] = line["lineNumNew"]; + delete line["lineNumNew"]; + } + + if (line["lineNum"]) { + line["lineNo"] = line["lineNum"]; + delete line["lineNum"]; + } } } } @@ -364,12 +384,16 @@ export class Project implements ProjectInfo { const info = await Project.getInfo(id, seed.addr); id = isRadicleId(id) ? id : info.id; - // Older versions of http-api don't include the ID. - if (!info.id) info.id = id; + let peers: Peer[] = []; - const peers: Peer[] = info.delegates - ? await Project.getRemotes(id, seed.addr) - : []; + if (window.HEARTWOOD) { + peers = await Project.getRemotes(id, seed.addr); + } else { + // Older versions of http-api don't include the ID. + if (!info.id) info.id = id; + + peers = info.delegates ? await Project.getRemotes(id, seed.addr) : []; + } let remote: Remote = { heads: info.head ? { [info.defaultBranch]: info.head } : {}, diff --git a/src/lib/registrar.ts b/src/lib/registrar.ts index bec7264de..430478eea 100644 --- a/src/lib/registrar.ts +++ b/src/lib/registrar.ts @@ -90,18 +90,8 @@ export async function getRegistration( getText(resolver, "com.github"), ]); - const [ - address, - avatar, - url, - id, - seedId, - seedHost, - seedGit, - seedApi, - twitter, - github, - ] = meta.filter(isFulfilled).map(r => (r.value ? r.value : undefined)); + const [address, avatar, url, id, seedId, seedHost, seedApi, twitter, github] = + meta.filter(isFulfilled).map(r => (r.value ? r.value : undefined)); const profile: EnsProfile = { name, @@ -119,7 +109,6 @@ export async function getRegistration( profile.seed = new Seed({ host: seedHost, id: seedId, - git: seedGit, addr: seedApi, }); } catch (e: any) { @@ -157,10 +146,9 @@ export async function getSeed( return null; } - const [id, host, git, api] = await Promise.all([ + const [id, host, api] = await Promise.all([ getText(resolver, "eth.radicle.seed.id"), getText(resolver, "eth.radicle.seed.host"), - getText(resolver, "eth.radicle.seed.git"), getText(resolver, "eth.radicle.seed.api"), ]); @@ -170,7 +158,7 @@ export async function getSeed( } try { - return new Seed({ host, id, git, addr: api }); + return new Seed({ host, id, addr: api }); } catch (e: any) { console.debug(e, host, id); return new InvalidSeed(id, host); diff --git a/src/lib/seed.ts b/src/lib/seed.ts index 723e55ee5..306877ab8 100644 --- a/src/lib/seed.ts +++ b/src/lib/seed.ts @@ -23,15 +23,16 @@ export class InvalidSeed { } } -export const defaultSeedPort = 8777; +export const defaultSeedPort = window.HEARTWOOD ? 8080 : 8777; export const defaultNodePort = 8776; +// TODO: Remove this after we have migrated to Heartwood. export const defaultGitPort = 443; export class Seed { valid = true as const; addr: { host: string; port: number | null }; - git: { host: string; port: number | null }; + git: { host: string; port: number | null }; // TODO: Remove this after we have migrated to Heartwood. node: { host: string; id: string; port: number }; version?: string; @@ -40,17 +41,21 @@ export class Seed { constructor(seed: { host: string; id: string; - git?: string | null; + git?: string | null; // TODO: Remove this after we have migrated to Heartwood. addr?: string | null; version?: string | null; }) { assert(isDomain(seed.host), `invalid seed host: ${seed.host}`); - assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`); + if (window.HEARTWOOD) { + assert(/^[a-zA-Z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`); + } else { + assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`); + } let _seed = null; - let _git = null; + let _git = null; // TODO: Remove this after we have migrated to Heartwood. let _seedPort: number | null = defaultSeedPort; - let _gitPort: number | null = defaultGitPort; + let _gitPort: number | null = defaultGitPort; // TODO: Remove this after we have migrated to Heartwood. if (seed.addr) { try { @@ -73,15 +78,17 @@ export class Seed { assert(isDomain(_seed), `invalid seed host ${_seed}`); } - if (seed.git) { - try { - const url = new URL(seed.git); - _git = url.hostname; - _gitPort = url.port ? Number(url.port) : null; - } catch { - _git = seed.git; + if (window.HEARTWOOD) { + if (seed.git) { + try { + const url = new URL(seed.git); + _git = url.hostname; + _gitPort = url.port ? Number(url.port) : null; + } catch { + _git = seed.git; + } + assert(isDomain(_git), `invalid seed git host ${_git}`); } - assert(isDomain(_git), `invalid seed git host ${_git}`); } this.emoji = getSeedEmoji(seed.host); @@ -89,10 +96,10 @@ export class Seed { // The `_seed` being more specific takes // precedence over the `host`, if available. _seed = _seed ?? seed.host; - _git = _git ?? seed.host; + _git = _git ?? seed.host; // TODO: Remove this after we have migrated to Heartwood. this.addr = { host: _seed, port: _seedPort }; - this.git = { host: _git, port: _gitPort }; + this.git = { host: _git, port: _gitPort }; // TODO: Remove this after we have migrated to Heartwood. this.node = { host: seed.host, id: seed.id, port: defaultNodePort }; if (seed.version) { @@ -121,10 +128,14 @@ export class Seed { ? await proj.Project.getDelegateProjects(id, this.addr, { perPage }) : await proj.Project.getProjects(this.addr, { perPage }); - return result.map((project: proj.ProjectInfo) => ({ - ...project, - id: project.id, - })); + if (window.HEARTWOOD) { + return result; + } else { + return result.map((project: proj.ProjectInfo) => ({ + ...project, + id: project.id, + })); + } } async getStats(): Promise<{ @@ -135,7 +146,11 @@ export class Seed { } static async getNode(host: Host): Promise<{ id: string }> { - return new Request("/peer", host).get(); + if (window.HEARTWOOD) { + return new Request("/node", host).get(); + } else { + return new Request("/peer", host).get(); + } } static async getInfo(host: Host): Promise<{ version: string }> { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index fbdf2f09c..ffc9d62c7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -105,7 +105,11 @@ export function formatSeedId(id: string): string { export function formatRadicleId(id: string): string { assert(isRadicleId(id)); - return id.substring(0, 14) + "…" + id.substring(id.length - 6, id.length); + if (window.HEARTWOOD) { + return id.substring(0, 10) + "…" + id.substring(id.length - 6, id.length); + } else { + return id.substring(0, 14) + "…" + id.substring(id.length - 6, id.length); + } } export function formatBalance(n: BigNumber, decimals?: number): string { @@ -245,7 +249,11 @@ export const formatTimestamp = ( // Check whether the input is a Radicle ID. export function isRadicleId(input: string): boolean { - return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input); + if (window.HEARTWOOD) { + return /^rad:[a-zA-Z0-9]+$/.test(input); + } else { + return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input); + } } // Check whether the input is a Radicle Peer ID. @@ -295,7 +303,11 @@ export function formatName(input: string, wallet: Wallet): string { // Parse a Radicle Id. export function parseRadicleId(id: string): string { - return id.replace(/^rad:[a-z]+:/, ""); + if (window.HEARTWOOD) { + return id.replace(/^rad:/, ""); + } else { + return id.replace(/^rad:[a-z]+:/, ""); + } } // Get amount of days passed between two dates without including the end date diff --git a/src/views/projects/Blob.svelte b/src/views/projects/Blob.svelte index f460afab0..79a559024 100644 --- a/src/views/projects/Blob.svelte +++ b/src/views/projects/Blob.svelte @@ -18,7 +18,7 @@ export let line: string | undefined = undefined; const fileExtension = blob.path.split(".").pop() ?? ""; - const lastCommit = blob.info.lastCommit; + const lastCommit = window.HEARTWOOD ? blob.lastCommit : blob.info.lastCommit; const parentDir = blob.path .match(/^.*\/|/) ?.values() @@ -213,7 +213,7 @@ {parentDir} ​ - {blob.info.name} + {window.HEARTWOOD ? blob.name : blob.info.name}
{#if isMarkdown} diff --git a/src/views/projects/Commit.svelte b/src/views/projects/Commit.svelte index 870b04802..21bfb4d31 100644 --- a/src/views/projects/Commit.svelte +++ b/src/views/projects/Commit.svelte @@ -73,5 +73,8 @@ {/if}
- + diff --git a/src/views/projects/Commit/CommitTeaser.svelte b/src/views/projects/Commit/CommitTeaser.svelte index 6ef01ae51..47be7cd24 100644 --- a/src/views/projects/Commit/CommitTeaser.svelte +++ b/src/views/projects/Commit/CommitTeaser.svelte @@ -94,7 +94,7 @@
- {#if commit.context.committer} + {#if !window.HEARTWOOD && commit.context.committer}
diff --git a/src/views/projects/Header.svelte b/src/views/projects/Header.svelte index 3660ad348..4888e5034 100644 --- a/src/views/projects/Header.svelte +++ b/src/views/projects/Header.svelte @@ -98,9 +98,12 @@ {revision} on:branchChanged={event => updateRevision(event.detail)} /> - {#if seed.git.host} + {#if window.HEARTWOOD && seed.addr.host} + + {:else if seed.git.host} {/if} + {#if seed.addr.host}
- + {#if !window.HEARTWOOD} + + {/if} + {#each issue.discussion as comment} - {#if comment.replies} -
- {#each comment.replies as reply} - - {/each} -
+ {#if !window.HEARTWOOD} + {#if comment.replies} +
+ {#each comment.replies as reply} + + {/each} +
+ {/if} {/if} {/each}
diff --git a/src/views/projects/Patch.svelte b/src/views/projects/Patch.svelte index 7b960916d..86c434a6a 100644 --- a/src/views/projects/Patch.svelte +++ b/src/views/projects/Patch.svelte @@ -138,8 +138,10 @@
{:else if activeTab === PatchTab.Diff && revision.changeset} onBrowse(e, revision.oid)} /> {:else if activeTab === PatchTab.Diff} diff --git a/src/views/projects/Patch/PatchTimeline.svelte b/src/views/projects/Patch/PatchTimeline.svelte index eaca11ec1..04dd8aa0e 100644 --- a/src/views/projects/Patch/PatchTimeline.svelte +++ b/src/views/projects/Patch/PatchTimeline.svelte @@ -66,11 +66,13 @@ {:else if element.type === TimelineType.Thread}
-
- {#each element.inner.replies as comment} - - {/each} -
+ {#if !window.HEARTWOOD} +
+ {#each element.inner.replies as comment} + + {/each} +
+ {/if}
{/if} {/each} diff --git a/src/views/projects/Tree.svelte b/src/views/projects/Tree.svelte index 1f86d6c42..eb30a07ce 100644 --- a/src/views/projects/Tree.svelte +++ b/src/views/projects/Tree.svelte @@ -3,8 +3,6 @@ import { createEventDispatcher } from "svelte"; - import { ObjectType } from "@app/lib/project"; - import File from "./Tree/File.svelte"; import Folder from "./Tree/Folder.svelte"; @@ -20,11 +18,11 @@ {#each tree.entries as entry (entry.path)} - {#if entry.info.objectType === ObjectType.Tree} + {#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"} @@ -32,7 +30,7 @@ onSelect({ detail: entry.path })} /> {/if} {/each} diff --git a/src/views/projects/Tree/Folder.svelte b/src/views/projects/Tree/Folder.svelte index e13889437..ca5aa72ef 100644 --- a/src/views/projects/Tree/Folder.svelte +++ b/src/views/projects/Tree/Folder.svelte @@ -2,7 +2,6 @@ import type { MaybeTree } from "@app/lib/project"; import Loading from "@app/components/Loading.svelte"; - import { ObjectType } from "@app/lib/project"; import { createEventDispatcher } from "svelte"; import File from "./File.svelte"; @@ -75,10 +74,10 @@ {:then tree} {#if tree} {#each tree.entries as entry (entry.path)} - {#if entry.info.objectType === ObjectType.Tree} + {#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"} { onSelectFile({ detail: entry.path }); }} /> diff --git a/src/views/registrations/View.svelte b/src/views/registrations/View.svelte index a8ab70635..3134fe663 100644 --- a/src/views/registrations/View.svelte +++ b/src/views/registrations/View.svelte @@ -124,7 +124,9 @@ name: "id", label: "Radicle", validate: "identity", - placeholder: "Radicle ID, eg. rad:git:hnrkqdpm9ub19oc8d…", + placeholder: window.HEARTWOOD + ? "Radicle ID, eg. rad:zKtT7DmF9H34KkvcKj…" + : "Radicle ID, eg. rad:git:hnrkqdpm9ub19oc8d…", description: "The local radicle identity associated with this name.", value: r.profile.id ?? "", editable: true, diff --git a/tests/e2e/project.spec.ts b/tests/e2e/project.spec.ts index 99b79606e..43c5acc9b 100644 --- a/tests/e2e/project.spec.ts +++ b/tests/e2e/project.spec.ts @@ -251,6 +251,10 @@ test("clone modal", async ({ page }) => { }); test("peer and branch switching", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(projectFixtureUrl); // Alice's peer. @@ -335,6 +339,10 @@ test("peer and branch switching", async ({ page }) => { }); test("only one modal can be open at a time", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(projectFixtureUrl); await page.getByTitle("Change peer").click(); @@ -366,6 +374,10 @@ test("only one modal can be open at a time", async ({ page }) => { }); test.describe("browser error handling", () => { + if (process.env.HEARTWOOD) { + test.skip(); + } + test("error appears when folder can't be loaded", async ({ page }) => { await page.route( `**/v1/projects/${ridPrefix}${rid}/tree/${aliceMainHead}/markdown/`, diff --git a/tests/e2e/project/commit.spec.ts b/tests/e2e/project/commit.spec.ts index aa36f06c7..07c571310 100644 --- a/tests/e2e/project/commit.spec.ts +++ b/tests/e2e/project/commit.spec.ts @@ -8,6 +8,10 @@ import { const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/2b32f6fe50090ebdb4cd7441e943330da3e6ff04`; test("navigation from commit list", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(projectFixtureUrl); await page.getByTitle("Change peer").click(); await page.locator("text=bob hyyzz9").click(); @@ -18,6 +22,10 @@ test("navigation from commit list", async ({ page }) => { }); test("relative timestamps", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.addInitScript(() => { window.initializeTestStubs = () => { window.e2eTestStubs.FakeTimers.install({ @@ -34,6 +42,10 @@ test("relative timestamps", async ({ page }) => { }); test("modified file", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(modifiedFileFixture); // Commit header. @@ -59,6 +71,10 @@ test("modified file", async ({ page }) => { }); test("created file", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto( `${projectFixtureUrl}/remotes/${bobRemote}/commits/d6318f7f3d9c15b8ac6dd52267c53220d00f0982`, ); @@ -69,6 +85,10 @@ test("created file", async ({ page }) => { }); test("deleted file", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto( `${projectFixtureUrl}/remotes/${bobRemote}/commits/cd13c2d9a8a930d64a82b6134b44d1b872e33662`, ); diff --git a/tests/e2e/project/commits.spec.ts b/tests/e2e/project/commits.spec.ts index 18956b343..74e45818a 100644 --- a/tests/e2e/project/commits.spec.ts +++ b/tests/e2e/project/commits.spec.ts @@ -6,6 +6,10 @@ import { } from "@tests/support/fixtures.js"; test("peer and branch switching", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(projectFixtureUrl); await page.locator('role=button[name="Commit count"]').click(); @@ -83,6 +87,10 @@ test("peer and branch switching", async ({ page }) => { }); test("verified badge", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.goto(projectFixtureUrl); await page.locator('role=button[name="Commit count"]').click(); @@ -101,6 +109,10 @@ test("verified badge", async ({ page }) => { }); test("relative timestamps", async ({ page }) => { + if (process.env.HEARTWOOD) { + test.skip(); + } + await page.addInitScript(() => { window.initializeTestStubs = () => { window.e2eTestStubs.FakeTimers.install({ diff --git a/tests/fixtures/seeds/palm-heartwood.tar.bz2 b/tests/fixtures/seeds/palm-heartwood.tar.bz2 new file mode 100644 index 000000000..cb2ccada6 Binary files /dev/null and b/tests/fixtures/seeds/palm-heartwood.tar.bz2 differ diff --git a/tests/support/fixtures.ts b/tests/support/fixtures.ts index addf60083..ffc6453a5 100644 --- a/tests/support/fixtures.ts +++ b/tests/support/fixtures.ts @@ -160,7 +160,11 @@ function log(text: string, label: string, outputLog: Stream.Writable) { } } -export function appConfigWithFixture() { +export const appConfigWithFixture = process.env.HEARTWOOD + ? configFixtureHeartwood + : configFixture; + +export function configFixture() { window.APP_CONFIG = { walletConnect: { bridge: "https://radicle.bridge.walletconnect.org", @@ -181,15 +185,41 @@ export function appConfigWithFixture() { }; } +export function configFixtureHeartwood() { + window.APP_CONFIG = { + walletConnect: { + bridge: "https://radicle.bridge.walletconnect.org", + }, + reactions: [], + seeds: { + pinned: [{ host: "0.0.0.0", emoji: "🚀" }], + }, + projects: { + pinned: [ + { + name: "source-browsing", + id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", + seed: "0.0.0.0", + }, + ], + }, + }; +} + export const aliceMainHead = "fcc929424b82984b7cbff9c01d2e20d9b1249842"; -export const aliceRemote = - "hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy"; +export const aliceRemote = process.env.HEARTWOOD + ? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi" + : "hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy"; + export const bobRemote = "hyyzz9w4ffg16zftjki3enajm4mkqkayb5ch1p6ns3f83np1hqkrp6"; -export const rid = "hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy"; -export const ridPrefix = "rad:git:"; +export const rid = process.env.HEARTWOOD + ? "zKtT7DmF9H34KkvcKj9PHW19WzjT" + : "hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy"; +export const ridPrefix = process.env.HEARTWOOD ? "rad:" : "rad:git:"; export const projectFixtureUrl = `/seeds/0.0.0.0/${ridPrefix}${rid}`; -export const seedPort = 8777; -export const seedVersion = "0.2.0"; -export const seedRemote = - "hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o"; +export const seedPort = process.env.HEARTWOOD ? 8080 : "8777"; +export const seedVersion = process.env.HEARTWOOD ? "0.1.0" : "0.2.0"; +export const seedRemote = process.env.HEARTWOOD + ? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi" + : "hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o"; diff --git a/tests/support/globalSetup.ts b/tests/support/globalSetup.ts index 617dff674..60cca10ee 100644 --- a/tests/support/globalSetup.ts +++ b/tests/support/globalSetup.ts @@ -9,24 +9,30 @@ export default async function globalSetup(_config: FullConfig): Promise { // 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 { - const palmTestFixtureSeedId = seedRemote; - 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"; + (process.env.HEARTWOOD + ? "👉 You can start it with `./scripts/run-httpd-with-fixtures`\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:${seedPort}`); - const data = await response.json(); - peerId = data.peer.id; + if (process.env.HEARTWOOD) { + const response = await fetch(`http://0.0.0.0:${seedPort}/api`); + const data = await response.json(); + peerId = data.node.id; + } else { + const response = await fetch(`http://0.0.0.0:${seedPort}`); + const data = await response.json(); + peerId = data.peer.id; + } } catch (err) { console.error(err); throw new Error(notRunningMessage); } - if (peerId !== palmTestFixtureSeedId) { + if (peerId !== seedRemote) { const wrongSeedMessage = `The server on port ${seedPort} doesn't have the right fixtures.\n`; throw new Error(wrongSeedMessage + notRunningMessage); } diff --git a/tests/unit/router.test.ts b/tests/unit/router.test.ts index 8f4acf3f3..a6bc3d9b6 100644 --- a/tests/unit/router.test.ts +++ b/tests/unit/router.test.ts @@ -101,11 +101,11 @@ describe("routeToPath", () => { params: { view: { resource: "tree" }, seed: "willow.radicle.garden", - id: "rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao", + id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", }, }, output: - "/seeds/willow.radicle.garden/rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao/tree", + "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/tree", description: "Seed Project Route", }, ])("$description", (route: any) => { @@ -219,8 +219,7 @@ describe("pathToRoute", () => { description: "registrations Submit Route", }, { - input: - "/seeds/willow.radicle.garden/rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao", + input: "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", output: { resource: "projects", params: { @@ -228,7 +227,7 @@ describe("pathToRoute", () => { seed: "willow.radicle.garden", profile: undefined, peer: undefined, - id: "rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao", + id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", }, }, description: "Seed Project Route", diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 65215fee5..490697355 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -37,9 +37,15 @@ describe("Format functions", () => { }); test("formatRadicleId", () => { - expect( - utils.formatRadicleId("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"), - ).toEqual("rad:git:hnrkem…d84c9o"); + if (process.env.HEARTWOOD) { + expect(utils.formatRadicleId("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT")).toEqual( + "rad:zKtT7D…19WzjT", + ); + } else { + expect( + utils.formatRadicleId("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"), + ).toEqual("rad:git:hnrkem…d84c9o"); + } }); test("formatRadicleId throw when wrong ID", () => { @@ -117,7 +123,9 @@ describe("String Assertions", () => { }); test.each([ - { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true }, + process.env.HEARTWOOD + ? { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true } + : { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true }, { id: "0x1234567890123456789012345678901234567890", expected: false }, ])("isRadicleId $id => $expected", ({ id, expected }) => { expect(utils.isRadicleId(id)).toEqual(expected); diff --git a/tests/visual/landingPage.spec.ts b/tests/visual/landingPage.spec.ts index 527e45ba3..b0846fe12 100644 --- a/tests/visual/landingPage.spec.ts +++ b/tests/visual/landingPage.spec.ts @@ -5,6 +5,16 @@ test.use({ }); test("landing page", async ({ page }) => { + await page.addInitScript(() => { + window.initializeTestStubs = () => { + window.e2eTestStubs.FakeTimers.install({ + now: new Date("November 24 2022 12:00:00").valueOf(), + shouldClearNativeTimers: true, + shouldAdvanceTime: false, + }); + }; + }); + await page.addInitScript(appConfigWithFixture); await page.goto("/", { waitUntil: "networkidle" }); await expect(page).toHaveScreenshot(); diff --git a/tests/visual/seed.spec.ts b/tests/visual/seed.spec.ts index 7fa0f49bf..fcfb9218c 100644 --- a/tests/visual/seed.spec.ts +++ b/tests/visual/seed.spec.ts @@ -1,6 +1,16 @@ import { test, expect } from "@tests/support/fixtures.js"; test("seed page", async ({ page }) => { + await 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("/seeds/radicle.local", { waitUntil: "networkidle" }); await expect(page).toHaveScreenshot(); }); diff --git a/vite.config.ts b/vite.config.ts index 389b123a7..7165b2151 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,7 @@ function defineConstants() { const constants = { VITEST: process.env.VITEST !== undefined, PLAYWRIGHT: process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined, + HEARTWOOD: process.env.HEARTWOOD !== undefined, }; // Don't overwrite HASH_ROUTING in Playwright tests, so we can control it