Add cypress tests

This commit is contained in:
Jonathan Staab 2023-10-12 16:16:38 -07:00
parent 744f42a65f
commit bc88d341d4
29 changed files with 349 additions and 222 deletions

7
cypress.config.ts Normal file
View File

@ -0,0 +1,7 @@
import {defineConfig} from "cypress"
export default defineConfig({
e2e: {
baseUrl: "http://localhost:5173",
},
})

21
cypress/e2e/feed.cy.ts Normal file
View File

@ -0,0 +1,21 @@
describe("feed interaction", () => {
beforeEach(() => {
cy.visit("/")
})
it("clicking a card works", () => {
cy.get(".cy-note-click-target:first").click()
cy.get(".modal .fa-heart")
cy.get(".modal .cy-modal-close").click()
cy.get(".modal").should("not.exist")
})
it("feed controls works", () => {
cy.get(".fa-sliders").click()
cy.get(".modal .cy-chip:first .fa-times").click()
cy.get(".modal").contains("Apply Filters").click()
cy.contains("Kinds 30023,")
cy.get(".card", {timeout: 30000})
})
})

12
cypress/e2e/login.cy.ts Normal file
View File

@ -0,0 +1,12 @@
describe("authenticated usage", () => {
beforeEach(() => {
cy.login()
})
it("works", () => {
cy.visit("/")
cy.get("svg.logo").click()
cy.get(".card").contains("Profile").click()
cy.get(".cy-person-name").contains("test account 12938740")
})
})

10
cypress/e2e/search.cy.ts Normal file
View File

@ -0,0 +1,10 @@
describe("search", () => {
it("works", () => {
cy.visit("/")
cy.get(".cy-top-nav .fa-search").click()
cy.get(".cy-top-nav input").type("hodlbod")
cy.get(".modal").contains("hodlbod").click()
cy.get(".modal").contains("following")
cy.get(".modal").contains("followers")
})
})

23
cypress/e2e/signup.cy.ts Normal file
View File

@ -0,0 +1,23 @@
describe("signup", () => {
beforeEach(() => {
cy.visit("/")
})
it("works", () => {
cy.get(".cy-top-nav").contains("Log In").click()
cy.get(".modal").contains("Sign Up").click()
cy.get(".modal").contains("Let's go!").click()
cy.get(".modal [name=name]").type("9sd2j3e0sd")
cy.get(".modal").contains("Continue").click()
cy.get(".modal").contains("Got it").click()
cy.get(".modal").contains("Continue").click()
cy.get(".modal").contains("Continue").click()
cy.wait(1000)
cy.get(".modal").contains("Skip and see your feed").click()
cy.get(".modal").should("not.exist")
cy.contains("From follows")
cy.get("svg.logo").click()
cy.get(".card").contains("Profile").click()
cy.get(".cy-person-name").contains("9sd2j3e0sd")
})
})

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,29 @@
const nsec = "nsec1er8narhat3xjypf46pksxlr6k4jrmv2e9psazjk0adynly0l4ltsepvmy5"
Cypress.Commands.add("login", () => {
cy.session(
nsec,
() => {
cy.visit("/login/privkey")
cy.get("input[type=password]").type(nsec, {log: false})
cy.get(".cy-login-submit").click()
cy.get(".modal", {timeout: 10_000}).should("not.exist")
cy.contains("Don't have an account?").should("not.exist")
},
{
validate: () => {
let pubkey
cy.window()
.then(w => {
pubkey = w.pubkey.get()
})
.then(() => {
expect(pubkey).to.equal(
"c853d879b7376dab1cdcd4faf235a05f680aae42ba620abdd95d619542a5a379"
)
})
},
}
)
})

20
cypress/support/e2e.ts Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands"
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -23,6 +23,7 @@
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"autoprefixer": "^10.4.13",
"cypress": "^13.3.1",
"eslint": "^8.33.0",
"eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.19",
@ -55,6 +56,7 @@
"insane": "^2.6.2",
"lru-cache": "^7.18.3",
"marked": "^5.1.0",
"normalize-url": "^8.0.0",
"nostr-tools": "^1.12.1",
"npm-run-all": "^4.1.5",
"paravel": "^0.3.7",

View File

@ -2,7 +2,7 @@
import "@fortawesome/fontawesome-free/css/fontawesome.css"
import "@fortawesome/fontawesome-free/css/solid.css"
import {nip19} from "nostr-tools"
import {nip19, getPublicKey, generatePrivateKey} from "nostr-tools"
import {pluck} from "ramda"
import {seconds, Fetch} from "hurdak"
import {tryFetch, hexToBech32, bech32ToHex, now} from "src/util/misc"
@ -223,7 +223,15 @@
// Globals
Object.assign(window, {...engine, nip19, bech32ToHex, hexToBech32, router})
Object.assign(window, {
...engine,
nip19,
bech32ToHex,
hexToBech32,
router,
getPublicKey,
generatePrivateKey,
})
// Theme

View File

@ -156,8 +156,8 @@
}} />
<div
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b
border-gray-6 bg-gray-7 px-2 text-gray-2">
class="cy-top-nav fixed top-0 z-10 flex h-16 w-full items-center justify-between
border-b border-gray-6 bg-gray-7 px-2 text-gray-2">
<div>
<div class="app-logo flex cursor-pointer items-center gap-2" on:click={toggleMenu}>
<!-- <img alt="App Logo" src={logoUrl} class="w-10" /> -->
@ -183,7 +183,7 @@
<div
class={cx(
"search-input pointer-events-none fixed top-0 z-10 w-full px-2 text-gray-1",
"flex h-16 items-center justify-end gap-4",
"cy-top-nav flex h-16 items-center justify-end gap-4",
{
"pr-16": $session,
"pr-28": !$session,

View File

@ -223,7 +223,7 @@
</div>
{#if muted}
<p class="border-l-2 border-solid border-gray-6 pl-4 text-gray-1">
You have muted this note.
You have hidden this note.
<Anchor
theme="anchor"
on:click={() => {
@ -233,10 +233,12 @@
{:else}
<NoteContent {anchorId} note={event} {showEntire} />
{/if}
<div class="cy-note-click-target h-px" />
<NoteActions
note={event}
bind:this={actions}
{removeFromContext}
{showMuted}
{replies}
{likes}
{zaps}

View File

@ -37,6 +37,7 @@
export let note: Event
export let reply
export let showMuted
export let showEntire
export let removeFromContext
export let replies
@ -102,7 +103,7 @@
let showDetails = false
let actions = []
$: disableActions = !$canSign || $muted
$: disableActions = !$canSign || ($muted && !showMuted)
$: like = like || find(propEq("pubkey", $session?.pubkey), likes)
$: allLikes = like ? likes.filter(n => n.id !== like?.id).concat(like) : likes
$: $likesCount = allLikes.length

View File

@ -79,7 +79,7 @@
{:else if quote}
{#if muted}
<p class="mb-1 py-24 text-center text-gray-5">
You have muted this note.
You have hidden this note.
<Anchor class="underline" on:click={unmute}>Show</Anchor>
</p>
{:else}

View File

@ -30,7 +30,7 @@
</script>
<div class={cx("flex items-center gap-1", $$props.class)}>
<span>{displayPerson($person)}</span>
<span class="cy-person-name">{displayPerson($person)}</span>
<div class="flex items-center gap-1 font-normal">
{#if $following}
<span class="px-2 py-1 text-xs">

View File

@ -15,13 +15,13 @@
loginWithExtension(await ext.getPublicKey())
boot()
} else {
router.at("login/privkey").open()
router.at("login/privkey").replaceModal()
}
})
const signUp = () => router.at("onboarding").open()
const signUp = () => router.at("onboarding").replaceModal()
const advancedLogIn = () => router.at("login/advanced").open()
const advancedLogIn = () => router.at("login/advanced").replaceModal()
document.title = "Log In"
</script>

View File

@ -131,6 +131,7 @@
<Spinner />
{:else if Object.values(currentRelays).length > 0}
<p>Currently searching:</p>
<Content gap="gap-4" size="inherit">
{#each Object.values(currentRelays) as relay}
<div class="h-12">
{#if relay}
@ -138,6 +139,7 @@
{/if}
</div>
{/each}
</Content>
{/if}
</Content>

View File

@ -33,7 +33,7 @@
<i slot="before" class="fa fa-key" />
</Input>
</div>
<Anchor theme="button" on:click={logIn}>Log In</Anchor>
<Anchor theme="button" class="cy-login-submit" on:click={logIn}>Log In</Anchor>
</div>
{#if !Capacitor.isNativePlatform()}
<p class="rounded border-2 border-solid border-warning bg-gray-8 px-6 py-3">

View File

@ -1,8 +1,9 @@
<script lang="ts">
import {onMount} from "svelte"
import {generatePrivateKey} from "nostr-tools"
import {closure} from "hurdak"
import {closure, sleep} from "hurdak"
import {fly} from "src/util/transition"
import Content from "src/partials/Content.svelte"
import OnboardingIntro from "src/app/views/OnboardingIntro.svelte"
import OnboardingProfile from "src/app/views/OnboardingProfile.svelte"
import OnboardingKey from "src/app/views/OnboardingKey.svelte"
@ -62,6 +63,13 @@
})
const signup = async noteContent => {
// Go to our home page
router.at("notes").push()
// Make things async since the `key` change in App.svelte prevents the modal
// animation from completing, and it gets stuck. This is a svelte bug
await sleep(10)
loginWithPrivateKey(privkey)
// Do this first so we know where to publish everything else
@ -78,12 +86,6 @@
// Start our notifications listener
listenForNotifications()
// Close all modals
router.clearModals()
// Go to our home page
router.at("notes").push()
}
onMount(() => {
@ -94,6 +96,7 @@
{#key stage}
<div in:fly={{y: 20}}>
<Content size="lg">
{#if stage === "intro"}
<OnboardingIntro {setStage} />
{:else if stage === "profile"}
@ -107,5 +110,6 @@
{:else if stage === "note"}
<OnboardingNote {setStage} {signup} />
{/if}
</Content>
</div>
{/key}

View File

@ -3,7 +3,6 @@
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import PersonSummary from "src/app/shared/PersonSummary.svelte"
import type {Person} from "src/engine"
import {env, mention, loadPeople, searchPeople} from "src/engine"
@ -29,18 +28,15 @@
$: results = reject((p: Person) => pubkeys.includes(p.pubkey), $searchPeople(q))
</script>
<Content>
<Content class="text-center">
<Heading>Find Your People</Heading>
<Heading class="text-center">Find Your People</Heading>
<p>
To get you started, weve added some interesting people to your follow list. You can update
your follows list at any time.
To get you started, weve added some interesting people to your follow list. You can update your
follows list at any time.
</p>
<div class="flex gap-2">
<Anchor theme="button" on:click={prev}><i class="fa fa-arrow-left" /></Anchor>
<Anchor theme="button-accent" class="flex-grow text-center" on:click={next}>Continue</Anchor>
</div>
</Content>
<div class="flex items-center gap-2">
<i class="fa fa-user-astronaut fa-lg" />
<h2 class="staatliches text-2xl">Your follows</h2>
@ -83,4 +79,3 @@
</div>
</PersonSummary>
{/each}
</Content>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
export let setStage
@ -10,16 +9,14 @@
const next = () => setStage("profile")
</script>
<Content size="lg" class="text-center">
<Heading>Create an Account</Heading>
<Heading class="text-center">Create an Account</Heading>
<p>
New to Nostr? Click <Anchor class="underline" external href={tutorialUrl}>here</Anchor> or watch
the video below for a crash course on what it is, and how to use it.
New to Nostr? Click <Anchor class="underline" external href={tutorialUrl}>here</Anchor> or watch the
video below for a crash course on what it is, and how to use it.
</p>
<video controls src={videoUrl} class="object-contain object-center" />
<p>
When youre ready to dive in, click below and well guide you through the process of creating an
account.
</p>
<Anchor theme="button-accent" on:click={next}>Let's go!</Anchor>
</Content>
<Anchor theme="button-accent" class="text-center" on:click={next}>Let's go!</Anchor>

View File

@ -5,7 +5,6 @@
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import {env} from "src/engine"
export let privkey
@ -21,8 +20,7 @@
}
</script>
<Content size="lg" class="text-center">
<Heading>Generate a Key</Heading>
<Heading class="text-center">Generate a Key</Heading>
<p>
Your private key is your password, and gives you total control over your Nostr account. We've
generated a fresh one for you below store it somewhere safe!
@ -36,4 +34,3 @@
<Anchor theme="button" on:click={prev}><i class="fa fa-arrow-left" /></Anchor>
<Anchor theme="button-accent" class="flex-grow text-center" on:click={next}>Got it</Anchor>
</div>
</Content>

View File

@ -3,7 +3,6 @@
import Compose from "src/app/shared/Compose.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
export let signup
export let setStage
@ -19,9 +18,8 @@
})
</script>
<Content size="lg">
<Heading class="text-center">Welcome to Nostr</Heading>
<p class="text-center">
<p>
Your're all set! If have any questions, just ask! People around these parts are always ready to
lend a hand.
</p>
@ -35,4 +33,3 @@
<Anchor class="text-center" on:click={skip}>
Skip and see your feed <i class="fa fa-arrow-right" />
</Anchor>
</Content>

View File

@ -4,7 +4,6 @@
import ImageInput from "src/partials/ImageInput.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
export let profile
export let setStage
@ -13,9 +12,8 @@
const next = () => setStage("key")
</script>
<Content size="lg">
<Heading class="text-center">Introduce Yourself</Heading>
<p class="text-center">
<p>
Give other people something to go on. Remember that "privacy is the power to selectively reveal
oneself to the world".
</p>
@ -32,16 +30,10 @@
</div>
<div class="flex flex-col gap-1">
<strong>Profile Picture</strong>
<ImageInput
bind:value={profile.picture}
icon="image-portrait"
maxWidth={480}
maxHeight={480} />
<p class="text-sm text-gray-1">Please be mindful of others and only use small images.</p>
<ImageInput bind:value={profile.picture} icon="image-portrait" maxWidth={480} maxHeight={480} />
</div>
</div>
<div class="flex gap-2">
<Anchor theme="button" on:click={prev}><i class="fa fa-arrow-left" /></Anchor>
<Anchor theme="button-accent" class="flex-grow text-center" on:click={next}>Continue</Anchor>
</div>
</Content>

View File

@ -34,15 +34,14 @@
)
</script>
<Content>
<div class="text-center">
<Heading>Get Connected</Heading>
<Heading class="text-center">Get Connected</Heading>
<p>
Nostr is a protocol, not a platform. This means that <i>you</i> choose where to store your data.
</p>
<p>
Select your preferred storage relays below, or click "continue" to use some reasonable defaults.
You can change your selection any time.
</p>
</div>
<div class="flex gap-2">
<Anchor theme="button" on:click={prev}><i class="fa fa-arrow-left" /></Anchor>
<Anchor theme="button-accent" class="flex-grow text-center" on:click={next}>Continue</Anchor>
@ -57,21 +56,19 @@
<span>No relays connected</span>
</div>
{:else}
<div class="grid grid-cols-1 gap-4">
<Content gap="gap-4" size="inherit">
{#each relays as relay (relay.url)}
<RelayCard {relay}>
<div slot="actions">
{#if relays.length > 1}
<button
class="flex items-center gap-3 text-gray-1"
on:click={() => removeRelay(relay)}>
<button class="flex items-center gap-3 text-gray-1" on:click={() => removeRelay(relay)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
{/if}
</div>
</RelayCard>
{/each}
</div>
</Content>
{/if}
<div class="flex items-center gap-2">
<i class="fa fa-earth-asia fa-lg" />
@ -80,6 +77,7 @@
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
<Content gap="gap-4" size="inherit">
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay}>
<div slot="actions">
@ -89,8 +87,8 @@
</div>
</RelayCard>
{/each}
</Content>
<small class="text-center">
Showing {Math.min($knownRelays.length - relays.length, 50)}
of {$knownRelays.length - relays.length} known relays
</small>
</Content>

View File

@ -4,10 +4,14 @@
export let theme = "dark"
export let onRemove = null
const className = cx($$props.class, "inline-block rounded-full border border-solid py-1 px-2", {
const className = cx(
$$props.class,
"inline-block rounded-full border border-solid py-1 px-2 cy-chip",
{
"border-gray-1": theme === "dark",
"border-gray-8": theme === "light",
})
}
)
</script>
<div class={className}>

View File

@ -66,7 +66,7 @@
<div
class="pointer-events-auto flex h-10 w-10 cursor-pointer items-center justify-center rounded-full
border border-solid border-accent-light bg-accent text-white transition-colors hover:bg-accent-light">
<i class="fa fa-times fa-lg" />
<i class="fa fa-times fa-lg cy-modal-close" />
</div>
{#if $modals.length > 1 && index > 0}
<div

View File

@ -3,6 +3,7 @@
"include": ["src/**/*"],
"compilerOptions": {
"baseUrl": ".",
"module": "esnext",
"paths": {
"src/*": ["src/*"]
},

BIN
yarn.lock

Binary file not shown.