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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ coverage/
.codex
.vscode
/test-results/
# Per-widget tests/<widget>/test-results/ folders are tracked as UI evidence
# baselines (mirrors citizen-claim-widget's pattern). The root /test-results/
# above is the Playwright transient trace/video output and stays ignored.
playwright-report/
**/storybook-static/
**/.yalc
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ GoodWidget/
- Story interaction checks: `pnpm test:storybook`.
- Playwright QA/state-flow checks: `pnpm test:demo`.
- Root Playwright runtime artifacts (trace/video/attachments): `/test-results/` (gitignored).
- Committable screenshot evidence: `tests/design-system/test-results/` and
`tests/widgets/<widget-name>/test-results/`.
- Nested widget Playwright test-runs (`tests/widgets/<widget-name>/test-results/`) are gitignored
transient run output. The canonical visual evidence for a widget lives in
`examples/storybook/src/stories/<widget>/screenshots/` (curated, deterministic,
generated by the screenshot regen script) — commit only that curated set.
- Detailed workflow, fixture behavior, and QA reporting template live in
[`docs/demo-environment.md`](docs/demo-environment.md) and [`docs/qa-guide.md`](docs/qa-guide.md).

Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ GoodWidget/
ui/ # Tamagui tokens, preset, themes, config assembly, manifest, primitives
embed/ # Web Component wrapper + CSS custom property bridge
claim-widget/ # Example widget package using core + ui + embed
goodreserve-widget/ # Reserve swap widget package using core + ui + embed

examples/
react-web/ # React web override and theming demo
Expand Down Expand Up @@ -69,6 +70,7 @@ GoodWidget/
@goodwidget/embed

@goodwidget/claim-widget -> depends on core + ui + embed
@goodwidget/goodreserve-widget -> depends on core + ui + embed
```

`@goodwidget/ui` is the leaf design-system package and must not depend on `@goodwidget/core`.
Expand Down Expand Up @@ -306,8 +308,6 @@ Key patterns in `packages/claim-widget/src/ClaimWidget.tsx`:

## Demo and Review Environment

<<<<<<< HEAD

### React web demo lab

`examples/react-web` is a route-based Vite + React + RN-web SPA that serves as
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A cross-platform mini app framework for building web3 widgets that run inside wa
| `@goodwidget/ui` | Tamagui-based themeable component library (React + React Native Web) |
| `@goodwidget/embed` | Web Component wrapper for embedding mini apps in any HTML page |
| `@goodwidget/claim-widget` | Sample publishable widget — React component + Web Component |
| `@goodwidget/goodreserve-widget` | Reserve swap widget package (buy/sell flow on Celo/XDC) |

## Quick Start

Expand Down Expand Up @@ -175,6 +176,7 @@ GoodWidget/
ui/ → @goodwidget/ui (component library, theme system)
embed/ → @goodwidget/embed (Web Component wrapper)
claim-widget/ → @goodwidget/claim-widget (sample publishable widget)
goodreserve-widget/ → @goodwidget/goodreserve-widget (reserve swap widget)
examples/
react-web/ → React demo with style override showcase
html/ → Plain HTML consuming a web component widget
Expand Down
37 changes: 2 additions & 35 deletions examples/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@
*/
import React from 'react'
import type { Preview } from '@storybook/react'
import { GoodWidgetProvider } from '@goodwidget/core'
import type { GoodWidgetConfig } from '@goodwidget/ui'
import { MiniAppShell } from '@goodwidget/ui'

interface StoryGoodWidgetParameters {
config?: GoodWidgetConfig
defaultTheme?: 'light' | 'dark'
disableProvider?: boolean
useShell?: boolean
}

const preview: Preview = {
parameters: {
Expand All @@ -37,33 +27,10 @@ const preview: Preview = {
},
},
decorators: [
(Story, context) => {
const params = (context.parameters.goodWidgetProvider ?? {}) as StoryGoodWidgetParameters
(Story) => {
const story = <Story />
const content =
params.useShell === false ? (
story
) : (
<MiniAppShell title="GoodWidgetDemos" headerRight={undefined}>
{story}
</MiniAppShell>
)

if (params.disableProvider) {
return params.useShell === false ? (
story
) : (
<MiniAppShell title="GoodWidgetDemos" headerRight={undefined}>
{story}
</MiniAppShell>
)
}

return (
<GoodWidgetProvider config={params.config} defaultTheme={params.defaultTheme ?? 'dark'}>
{content}
</GoodWidgetProvider>
)
return <>{story}</>
},
],
}
Expand Down
11 changes: 11 additions & 0 deletions examples/storybook/.storybook/test-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { TestRunnerConfig } from '@storybook/test-runner'

const config: TestRunnerConfig = {
setup() {
// Increase Jest timeout from the default 15000ms to 60000ms
// to prevent cold-start compilation timeouts in CI/test runner.
jest.setTimeout(60000)
},
}

export default config
1 change: 1 addition & 0 deletions examples/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@goodwidget/ui": "workspace:*",
"@goodwidget/claim-widget-theme-demo": "workspace:*",
"@goodwidget/citizen-claim-widget": "workspace:*",
"@goodwidget/goodreserve-widget": "workspace:*",
"@goodwidget/governance-widget": "workspace:*",
"@goodwidget/streaming-widget": "workspace:*",
"@goodwidget/staking-migration-widget": "workspace:*",
Expand Down
168 changes: 168 additions & 0 deletions examples/storybook/src/fixtures/goodReserveWidgetMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { ReserveSwapWidgetAdapterState } from '@goodwidget/goodreserve-widget'

// Deterministic reserve widget state fixtures used by Storybook and CI tests.
export const reserveWidgetMockStates: Record<string, Partial<ReserveSwapWidgetAdapterState>> = {
noProvider: {
status: 'no_provider',
hasProvider: false,
chainId: null,
address: null,
},
unsupportedChain: {
status: 'unsupported_chain',
hasProvider: true,
chainId: 8453,
address: '0x1111111111111111111111111111111111111111',
},
sdkInitializing: {
status: 'sdk_initializing',
hasProvider: true,
chainId: 42220,
address: '0x1111111111111111111111111111111111111111',
},
idleBuy: {
status: 'idle',
chainId: 42220,
address: '0x1111111111111111111111111111111111111111',
hasProvider: true,
tokenInSymbol: 'USDm',
tokenOutSymbol: 'G$',
tokenInBalance: '120.00',
tokenOutBalance: '10340.22',
inputAmount: '',
direction: 'buy',
},
amountEditing: {
status: 'amount_editing',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
tokenInBalance: '120.00',
tokenOutBalance: '10340.22',
},
quoteLoading: {
status: 'quote_loading',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
tokenInBalance: '120.00',
tokenOutBalance: '10340.22',
},
quoteReady: {
status: 'quote_ready',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
tokenInBalance: '120.00',
quote: {
outputAmount: '108.2500',
price: '4.33000',
minimumReceived: '108.1417',
priceImpactPercent: 'N/A',
exitContributionPercent: '0%',
},
},
quoteError: {
status: 'quote_error',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
error: 'Reserve quote failed. Try again in a moment.',
},
insufficientBalance: {
status: 'insufficient_balance',
chainId: 42220,
hasProvider: true,
inputAmount: '9999',
tokenInBalance: '120.00',
warning: 'Input exceeds your available token balance.',
},
slippageSelection: {
status: 'slippage_selection',
chainId: 42220,
hasProvider: true,
slippagePercent: 0.5,
},
confirmDialog: {
status: 'confirm_dialog',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
quote: {
outputAmount: '108.2500',
price: '4.33000',
minimumReceived: '108.1417',
priceImpactPercent: 'N/A',
exitContributionPercent: '0%',
},
},
swapPending: {
status: 'swap_pending',
chainId: 42220,
hasProvider: true,
inputAmount: '25',
quote: {
outputAmount: '108.2500',
price: '4.33000',
minimumReceived: '108.1417',
priceImpactPercent: 'N/A',
exitContributionPercent: '0%',
},
},
swapSuccess: {
status: 'swap_success',
chainId: 42220,
hasProvider: true,
tokenOutSymbol: 'G$',
tokenOutBalance: '12,500',
// Post-swap reality: quote is cleared and the received amount is preserved
// in lastSwapOutput (distinct from the wallet balance).
lastSwapOutput: '10,230',
quote: null,
txHash: '0xabc1230000000000000000000000000000000000000000000000000000000000',
},
swapError: {
status: 'swap_error',
chainId: 42220,
hasProvider: true,
error: 'Swap reverted due to reserve limits.',
},
sellQuoteReady: {
status: 'quote_ready',
chainId: 42220,
hasProvider: true,
direction: 'sell',
tokenInSymbol: 'G$',
tokenOutSymbol: 'USDm',
tokenInBalance: '300.00',
tokenOutBalance: '84.00',
inputAmount: '40',
quote: {
outputAmount: '8.9231',
price: '0.22308',
minimumReceived: '8.9142',
priceImpactPercent: 'N/A',
exitContributionPercent: '0%',
},
},
// Buy-ready state on XDC (chain 50) — exercises the dynamic network label and
// the USDC stable-token symbol used on XDC.
xdcQuoteReady: {
status: 'quote_ready',
chainId: 50,
hasProvider: true,
direction: 'buy',
tokenInSymbol: 'USDC',
tokenOutSymbol: 'G$',
tokenInBalance: '500.00',
tokenOutBalance: '0.00',
inputAmount: '50',
quote: {
outputAmount: '216.5000',
price: '4.33000',
minimumReceived: '216.2835',
priceImpactPercent: 'N/A',
exitContributionPercent: '0%',
},
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import React, { useState } from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { expect, screen, within, userEvent } from '@storybook/test'
import { expect, within, userEvent } from '@storybook/test'
import { Drawer, Card, Heading, Text, Button, ButtonText, YStack } from '@goodwidget/ui'

const meta: Meta = {
Expand All @@ -20,7 +20,6 @@ type Story = StoryObj
/** Controlled Drawer triggered by a button. */
export const Default: Story = {
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [open, setOpen] = useState(false)
return (
<Card data-testid="Drawer-trigger" style={{ width: 320 }}>
Expand All @@ -45,7 +44,9 @@ export const Default: Story = {
const canvas = within(canvasElement)
const trigger = canvas.getByRole('button', { name: /open drawer/i })
await userEvent.click(trigger)
// Drawer content is portaled, so query the document-level screen instead of the canvas.
await expect(await screen.findByRole('button', { name: /close/i })).toBeVisible()
// After clicking, the Close button should appear inside the Drawer
// The Drawer is rendered in a portal, so we need to search the entire document
const closeButton = await within(document.body).findByRole('button', { name: /close/i })
await expect(closeButton).toBeDefined()
},
}
Loading
Loading