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
53 changes: 53 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Test Suite

on:
pull_request:
push:
branches: [main]

jobs:
unit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run unit tests
run: bun run test:unit
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: coverage
if-no-files-found: ignore

e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
run: bun run test:e2e
- name: Upload Playwright artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-artifacts
path: |
test-results
playwright-report
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
playwright-report
test-results
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ To start development:
4. Install dependencies `bun install`.
5. Start `bun run dev`.

### Testing

The project now uses a two-layer automated test suite:

1. Unit and integration tests with `bun test`
2. Browser UI-state tests with Playwright

Run:

- `bun run test:unit` for library/unit/integration tests with coverage output
- `bun run test:e2e` for deterministic browser tests
- `bun run test:all` to run both

For local Playwright setup:

1. `bun install`
2. `bunx playwright install chromium`

## Structure

Lintent is built around a single page [/src/routes/+page.svelte](/src/routes/+page.svelte).
Expand Down
9 changes: 9 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions drizzle/0004_store_transaction_receipts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE "transaction_receipts" (
"id" text PRIMARY KEY NOT NULL,
"chain_id" bigint NOT NULL,
"tx_hash" text NOT NULL,
"receipt" text NOT NULL,
"created_at" bigint NOT NULL
);
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"build": "vite build",
"preview": "vite preview",
"prepare": "husky",
"test": "bun test",
"test": "bun run test:all",
"test:unit": "bun test tests/unit tests/db.test.ts --coverage",
"test:e2e": "bunx playwright test",
"test:e2e:headed": "bunx playwright test --headed",
"test:e2e:ui": "bunx playwright test --ui",
"test:all": "bun run test:unit && bun run test:e2e",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
Expand All @@ -22,6 +27,7 @@
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"@tailwindcss/vite": "^4.0.0",
"@types/bun": "^1.3.8",
"@playwright/test": "^1.55.0",
"drizzle-kit": "^0.31.9",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
Expand Down
29 changes: 29 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "tests/e2e",
timeout: 45_000,
expect: {
timeout: 10_000
},
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: "http://127.0.0.1:4173",
trace: "retain-on-failure",
video: "retain-on-failure",
screenshot: "only-on-failure"
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] }
}
],
webServer: {
command: "bunx vite dev --host 127.0.0.1 --port 4173",
url: "http://127.0.0.1:4173",
reuseExistingServer: !process.env.CI,
timeout: 120_000
}
});
67 changes: 52 additions & 15 deletions src/lib/components/AwaitButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,78 @@
name,
awaiting,
buttonFunction,
baseClass = ["rounded border px-4 h-8 text-xl font-bold"],
hoverClass = ["text-gray-600 hover:text-blue-600"],
lazyClass = ["text-gray-300"]
size = "md",
variant = "default",
fullWidth = false,
baseClass = [],
hoverClass = [],
lazyClass = []
}: {
name: Snippet;
awaiting: Snippet;
buttonFunction: () => Promise<any>;
buttonFunction: () => Promise<unknown>;
size?: "sm" | "md";
variant?: "default" | "success" | "warning" | "muted";
fullWidth?: boolean;
baseClass?: string[];
hoverClass?: string[];
lazyClass?: string[];
} = $props();
let buttonPromise: Promise<any> | undefined = $state();
const sizeClass = $derived(size === "sm" ? "h-7 px-2 text-xs" : "h-8 px-3 text-sm");
const variantBaseClass = $derived.by(() => {
if (variant === "success") return "border-emerald-200 bg-emerald-50 text-emerald-900";
if (variant === "warning") return "border-amber-200 bg-amber-50 text-amber-900";
if (variant === "muted") return "border-gray-200 bg-gray-100 text-gray-500";
return "border-gray-200 bg-white text-gray-700";
});
const variantHoverClass = $derived.by(() => {
if (variant === "success") return "hover:border-emerald-300 hover:bg-emerald-100";
if (variant === "warning") return "hover:border-amber-300 hover:bg-amber-100";
if (variant === "muted") return "";
return "hover:border-sky-300 hover:text-sky-700";
});
const defaultBase = $derived([
sizeClass,
"rounded border font-semibold",
variantBaseClass,
fullWidth ? "w-full" : ""
]);
const defaultHover = $derived(variantHoverClass ? [variantHoverClass] : []);
const defaultLazy = [
"cursor-not-allowed",
variant === "muted" ? "text-gray-500" : "text-gray-400"
];
let buttonPromise: Promise<unknown> | undefined = $state();
const run = () => {
buttonPromise = buttonFunction().catch((error) => {
console.error("AwaitButton action failed", error);
throw error;
});
};
</script>

{#await buttonPromise}
<button type="button" class={[...baseClass, ...lazyClass]} disabled>
<button
type="button"
class={[...defaultBase, ...baseClass, ...defaultLazy, ...lazyClass]}
disabled
>
{@render awaiting()}
</button>
{:then _}
{:then}
<button
onclick={() => (buttonPromise = buttonFunction())}
onclick={run}
type="button"
class={[...baseClass, ...hoverClass]}
class={[...defaultBase, ...baseClass, ...defaultHover, ...hoverClass]}
>
{@render name()}
</button>
{:catch error}
{:catch}
<button
onclick={() => (buttonPromise = buttonFunction())}
onclick={run}
type="button"
class={[...baseClass, ...hoverClass]}
class={[...defaultBase, ...baseClass, ...defaultHover, ...hoverClass]}
>
{@render name()}
</button>
{@html (() => {
console.error(error);
})()}
{/await}
28 changes: 21 additions & 7 deletions src/lib/components/GetQuote.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@
}, 1000);
});

$effect(() => {
if (typeof window === "undefined") return;
const onOnline = () => updateQuote();
window.addEventListener("online", onOnline);
return () => window.removeEventListener("online", onOnline);
});

$effect(() => {
quoteExpires;
if (quoteExpires === 0) {
Expand All @@ -100,27 +107,34 @@
let quoteRequest: Promise<void> = $state(Promise.resolve());
</script>

<div class="relative my-1 flex w-full items-center justify-center text-center align-middle">
<div class="relative flex w-full items-center justify-center text-center align-middle">
{#await quoteRequest}
<div class="relative h-7 w-full animate-pulse rounded border px-2 font-bold">Fetch Quote</div>
<div
data-testid="quote-loading"
class="relative h-6 w-full rounded border border-gray-200 bg-white px-2 text-xs leading-6 font-semibold text-gray-500"
>
Quote
</div>
{:then _}
<!-- Button gradually shows how long until it is expired by fill background -->
{#if quoteExpires !== 0}
<div
class="absolute top-0 left-0 h-7 rounded bg-blue-200 transition-all"
class="absolute top-0 left-0 h-6 rounded bg-sky-100 transition-all"
style="width: {width}%"
></div>
<button
class="relative h-7 w-full cursor-pointer rounded border px-2 font-bold hover:text-blue-800"
onclick={updateQuote}>Fetch Quote</button
data-testid="quote-button"
class="relative h-6 w-full cursor-pointer rounded border border-gray-200 bg-white px-2 text-xs font-semibold text-gray-700 hover:border-sky-300 hover:text-sky-700"
onclick={updateQuote}>Quote</button
>
{:else}
<div
class="absolute top-0 left-0 h-7 rounded bg-red-200 transition-all"
class="absolute top-0 left-0 h-6 rounded bg-rose-100 transition-all"
style="width: 100%"
></div>
<button
class="relative h-7 w-full cursor-pointer rounded border px-2 font-bold hover:text-blue-800"
data-testid="quote-button"
class="relative h-6 w-full cursor-pointer rounded border border-rose-200 bg-white px-2 text-xs font-semibold text-rose-700 hover:border-rose-300"
onclick={updateQuote}>No Quote</button
>
{/if}
Expand Down
Loading
Loading