Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
cache: npm

- run: npm ci
- run: npm ci --legacy-peer-deps

- run: npm test

Expand All @@ -38,7 +37,6 @@ jobs:
cache: npm

- run: npm ci
- run: npm ci --legacy-peer-deps

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox webkit
Expand All @@ -50,25 +48,21 @@ jobs:
NEXT_PUBLIC_STREAM_CONTRACT_ID_MAINNET: ''

- name: Run Playwright tests
# --update-snapshots generates missing baselines on first run so the
# job does not fail when new visual tests are added without pre-committed
# PNG baselines. On subsequent runs the existing PNGs are used as-is.
run: npx playwright test --update-snapshots
# Visual baselines are generated by the "Update Playwright Snapshots"
# workflow (workflow_dispatch). Until they are committed, run the
# functional e2e tests and skip screenshot assertions instead of
# silently regenerating (and so never actually comparing) them.
run: |
if find e2e/__screenshots__ -name '*.png' | grep -q .; then
npx playwright test
else
echo "::warning::No visual baselines committed; skipping screenshot assertions."
npx playwright test --ignore-snapshots
fi
env:
NEXT_PUBLIC_STREAM_CONTRACT_ID_TESTNET: ''
NEXT_PUBLIC_STREAM_CONTRACT_ID_MAINNET: ''

- name: Commit generated snapshots
# If new baseline PNGs were written, commit and push them so the next
# CI run can diff against them. Only runs when there are staged changes.
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add e2e/__screenshots__/
git diff --cached --quiet || git commit -m "test: update playwright visual snapshots [skip ci]" && git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:

- name: Post contract audit summary
if: always()
# Fork PRs get a read-only token; the summary is best-effort.
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
Expand Down Expand Up @@ -79,6 +81,9 @@ jobs:
frontend-audit:
name: Frontend Audit
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- uses: actions/checkout@v4
Expand All @@ -91,7 +96,13 @@ jobs:
- run: npm ci

- name: npm audit
run: npm audit --audit-level=high 2>&1 | tee /tmp/npm-audit.txt; exit ${PIPESTATUS[0]}
# Fail on high/critical issues in shipped (production) dependencies, and
# on critical issues anywhere. Dev-only tooling currently carries a high
# advisory (braces) with no patched release upstream.
run: |
set -o pipefail
npm audit --omit=dev --audit-level=high 2>&1 | tee /tmp/npm-audit.txt
npm audit --audit-level=critical 2>&1 | tee -a /tmp/npm-audit.txt

- name: ESLint security
run: npm run lint 2>&1 | tee /tmp/eslint-output.txt; exit ${PIPESTATUS[0]}
Expand All @@ -103,6 +114,8 @@ jobs:

- name: Post frontend audit summary
if: always()
# Fork PRs get a read-only token; the summary is best-effort.
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
# Secrets are unavailable to fork/Dependabot PRs (and unset until Vercel
# is configured); deploy steps are skipped then, the build still runs.
HAS_VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN != '' }}

steps:
- uses: actions/checkout@v4
Expand All @@ -33,11 +36,12 @@ jobs:
NEXT_PUBLIC_APP_ENV: staging

- name: Install Vercel CLI
if: env.HAS_VERCEL_TOKEN == 'true'
run: npm install -g vercel@latest

# Deploy preview for PRs, production-like staging deploy for staging branch
- name: Deploy to Vercel (staging branch)
if: github.ref == 'refs/heads/staging'
if: github.ref == 'refs/heads/staging' && env.HAS_VERCEL_TOKEN == 'true'
run: |
vercel deploy \
--token ${{ secrets.VERCEL_TOKEN }} \
Expand All @@ -49,7 +53,7 @@ jobs:
echo "Deployed to: $(cat deployment-url.txt)"

- name: Deploy preview (PR)
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && env.HAS_VERCEL_TOKEN == 'true'
run: |
vercel deploy \
--token ${{ secrets.VERCEL_TOKEN }} \
Expand All @@ -62,7 +66,8 @@ jobs:
echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_ENV

- name: Comment PR with preview URL
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && env.HAS_VERCEL_TOKEN == 'true'
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
Expand Down
13 changes: 2 additions & 11 deletions .github/workflows/update-snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ jobs:
update-snapshots:
name: Regenerate visual baselines
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
# Use a PAT so the commit push has write access
token: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/setup-node@v4
with:
Expand All @@ -36,15 +36,6 @@ jobs:
NEXT_PUBLIC_STREAM_CONTRACT_ID_TESTNET: ""
NEXT_PUBLIC_STREAM_CONTRACT_ID_MAINNET: ""

- name: Start app
run: npm run start &
env:
NEXT_PUBLIC_STREAM_CONTRACT_ID_TESTNET: ""
NEXT_PUBLIC_STREAM_CONTRACT_ID_MAINNET: ""

- name: Wait for app to be ready
run: npx wait-on http://localhost:3000 --timeout 60000

- name: Update snapshots
run: npx playwright test --update-snapshots
env:
Expand Down
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ contracts/target/
# Soroban / contract test snapshots (auto-generated, not source — regenerated by cargo test)
contracts/**/test_snapshots/

# Vitest snapshots and coverage
*.snap
**/__snapshots__/
# Vitest coverage
coverage/

# TypeScript build info cache
Expand Down
2 changes: 1 addition & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This guide covers deploying the FlowStar streaming contract to Stellar testnet o
```bash
cargo install stellar-cli --locked
```
- **Node.js 18+** for the frontend
- **Node.js 22.22+** for the frontend

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ cargo test

### Prerequisites

- Node.js 18+
- Node.js 22.22+ (matches CI)
- [Freighter](https://www.freighter.app/) browser extension (set to Testnet)

### Install & run
Expand Down
17 changes: 12 additions & 5 deletions __tests__/components/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ vi.mock('@/hooks/use-hidden-streams', () => ({
}))

vi.mock('next/link', () => ({
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
Expand Down Expand Up @@ -163,9 +167,7 @@ function makeStream(overrides: Partial<StreamData> = {}): StreamData {
}

/** Minimal useStreams return value for the given arrays. */
function streamsResult(
overrides: Partial<ReturnType<typeof mockUseStreams>> = {},
) {
function streamsResult(overrides: Partial<ReturnType<typeof mockUseStreams>> = {}) {
return {
sent: [],
received: [],
Expand Down Expand Up @@ -194,6 +196,7 @@ beforeEach(() => {
mockUseHiddenStreams.mockReturnValue({
hiddenIds: new Set<string>(),
blockedSenders: new Set<string>(),
pinnedIds: new Set<string>(),
})
mockUseStreams.mockReturnValue(streamsResult())
})
Expand Down Expand Up @@ -327,7 +330,11 @@ describe('populated state', () => {
// ─── Tab-filter views ─────────────────────────────────────────────────────────

describe('tab switching', () => {
const sentStream = makeStream({ id: 'tab-sent', sender: WALLET_ADDRESS, recipient: RECIPIENT_ADDRESS })
const sentStream = makeStream({
id: 'tab-sent',
sender: WALLET_ADDRESS,
recipient: RECIPIENT_ADDRESS,
})
const recvStream = makeStream({ id: 'tab-recv', sender: OTHER_SENDER, recipient: WALLET_ADDRESS })

beforeEach(() => {
Expand Down
30 changes: 15 additions & 15 deletions __tests__/components/hero.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { render, screen, fireEvent, within } from '@testing-library/react'
import React from 'react'
import { LandingHeader, Hero } from '@/components/landing/hero'

Expand All @@ -24,7 +24,7 @@ vi.mock('@/components/brand', () => ({
}))

vi.mock('@/components/ui/button', () => ({
Button: ({ children, asChild, nativeButton, size, variant, ...props }: any) => {
Button: ({ children, asChild, size, variant, ...props }: any) => {
if (asChild) {
// forward button-like props to the single child
const child = React.Children.only(children) as React.ReactElement
Expand All @@ -38,7 +38,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children, open, onOpenChange }: any) => (
<div data-testid="dropdown-menu" data-open={open}>
{React.Children.map(children, (child) =>
React.cloneElement(child as React.ReactElement, { _onOpenChange: onOpenChange }),
React.cloneElement(child as React.ReactElement<any>, { _onOpenChange: onOpenChange }),
)}
</div>
),
Expand All @@ -56,9 +56,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
</button>
)
},
DropdownMenuContent: ({ children }: any) => (
<div data-testid="dropdown-content">{children}</div>
),
DropdownMenuContent: ({ children }: any) => <div data-testid="dropdown-content">{children}</div>,
DropdownMenuItem: ({ children, render: renderProp }: any) =>
renderProp ? (
<div data-testid="dropdown-item">{renderProp}</div>
Expand All @@ -79,18 +77,22 @@ describe('LandingHeader', () => {
expect(screen.getByTestId('brand')).toBeInTheDocument()
})

// The mobile dropdown links are always rendered by the dropdown stub, so
// scope desktop-nav assertions to the <nav> element.
it('renders all desktop nav links', () => {
render(<LandingHeader />)
expect(screen.getByRole('link', { name: 'Features' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'How it works' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Use cases' })).toBeInTheDocument()
const nav = within(screen.getByRole('navigation'))
expect(nav.getByRole('link', { name: 'Features' })).toBeInTheDocument()
expect(nav.getByRole('link', { name: 'How it works' })).toBeInTheDocument()
expect(nav.getByRole('link', { name: 'Use cases' })).toBeInTheDocument()
})

it('desktop nav links point to the correct hash anchors', () => {
render(<LandingHeader />)
expect(screen.getByRole('link', { name: 'Features' })).toHaveAttribute('href', '#features')
expect(screen.getByRole('link', { name: 'How it works' })).toHaveAttribute('href', '#how')
expect(screen.getByRole('link', { name: 'Use cases' })).toHaveAttribute('href', '#use-cases')
const nav = within(screen.getByRole('navigation'))
expect(nav.getByRole('link', { name: 'Features' })).toHaveAttribute('href', '#features')
expect(nav.getByRole('link', { name: 'How it works' })).toHaveAttribute('href', '#how')
expect(nav.getByRole('link', { name: 'Use cases' })).toHaveAttribute('href', '#use-cases')
})

it('renders the "Open app" CTA link pointing to /app', () => {
Expand All @@ -102,9 +104,7 @@ describe('LandingHeader', () => {

it('renders the mobile nav trigger button with correct aria-label', () => {
render(<LandingHeader />)
expect(
screen.getByRole('button', { name: 'Open navigation menu' }),
).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Open navigation menu' })).toBeInTheDocument()
})

it('shows the Menu icon when mobile nav is closed', () => {
Expand Down
21 changes: 16 additions & 5 deletions __tests__/components/landing/live-stream-preview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ vi.mock('@/hooks/use-now', () => ({
}))

vi.mock('@/components/ui/progress-bar', () => ({
ProgressBar: ({ value, indeterminateShimmer }: { value: number; indeterminateShimmer?: boolean }) => (
<div data-testid="progress-bar" data-value={value} data-shimmer={indeterminateShimmer ? 'true' : 'false'} />
ProgressBar: ({
value,
indeterminateShimmer,
}: {
value: number
indeterminateShimmer?: boolean
}) => (
<div
data-testid="progress-bar"
data-value={value}
data-shimmer={indeterminateShimmer ? 'true' : 'false'}
/>
),
}))

Expand Down Expand Up @@ -106,9 +116,10 @@ describe('LiveStreamPreview', () => {
const progressBar1 = screen.getByTestId('progress-bar')
const value1 = parseFloat(progressBar1.getAttribute('data-value') || '0')

// Advance time by 10 seconds and re-render
vi.setSystemTime(new Date((NOW_SEC + 10) * 1000))
mockUseNow.mockReturnValue(NOW_SEC + 10)
// Advance time by 1 hour and re-render (progress is quantized to 0.01%,
// roughly 4 minutes of a 30-day stream, so a few seconds is not enough)
vi.setSystemTime(new Date((NOW_SEC + 3600) * 1000))
mockUseNow.mockReturnValue(NOW_SEC + 3600)
rerender(<LiveStreamPreview />)

const progressBar2 = screen.getByTestId('progress-bar')
Expand Down
Loading
Loading