Archipelago v1.7.129-alpha
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Content surfaces', () => {
|
||||
test('empty state shows when no conversation selected', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const main = page.locator('main.path-glass-card')
|
||||
await expect(main).toBeVisible()
|
||||
})
|
||||
|
||||
test('chat can receive input', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await input.click()
|
||||
await input.pressSequentially('Recommend some films')
|
||||
await expect(input).toHaveValue('Recommend some films', { timeout: 3000 })
|
||||
})
|
||||
|
||||
test('films surface: films conversation loads and shows film cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
await page.getByRole('button', { name: /View all \d+ films/i }).click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('films surface: clicking assistant bubble opens panel', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('magazine surface: BIP brief shows sections', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'BIP 110 brief' }).click()
|
||||
await expect(page.getByText(/BIP 110|Pro camp|Summary/i).first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: 'View brief' }).click()
|
||||
await expect(page.getByText(/AI Brief|Summary|Pro camp/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('songs surface: songs conversation shows song cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Music recommendations' }).click()
|
||||
await expect(page.getByText('Never Meant').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ songs/i }).click()
|
||||
await expect(page.locator('main').getByText('Never Meant').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('podcasts surface: podcasts conversation shows podcast cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Bitcoin podcasts' }).click()
|
||||
await expect(page.getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ podcasts/i }).click()
|
||||
await expect(page.locator('main').getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('websites surface: websites tab shows link cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Bitcoin resources' }).click()
|
||||
await expect(page.getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ websites/i }).click()
|
||||
await expect(page.locator('main').getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('news surface: news conversation shows articles', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Latest Bitcoin news' }).click()
|
||||
await expect(page.getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ articles/i }).click()
|
||||
await expect(page.locator('main').getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Chat interactions', () => {
|
||||
test('sends a message and receives streaming response', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.waitForLoadState('networkidle')
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await input.click()
|
||||
await input.fill('Hello')
|
||||
await input.press('Enter')
|
||||
// User message should appear in chat
|
||||
await expect(page.getByText('Hello').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('content panel shows film cards when AI mentions films', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Film cards should be visible inline in assistant message
|
||||
await expect(page.getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
// Open panel via "View all" button
|
||||
await page.getByRole('button', { name: /View all \d+ films/i }).click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('clicking a film card opens detail view', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open panel
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
const filmCard = page.locator('main').getByText('Blade Runner 2049').first()
|
||||
await expect(filmCard).toBeVisible({ timeout: 5000 })
|
||||
// Click film card to open detail
|
||||
await filmCard.click()
|
||||
// Detail view should show film metadata
|
||||
await expect(page.getByText(/Denis Villeneuve|2017|Sci-Fi/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('mobile viewport shows full-screen overlay for content', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 })
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open panel on mobile
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
// Content should be visible as overlay on mobile
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('stop button halts generation', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// When not streaming, stop button should not be visible
|
||||
const stopButton = page.getByRole('button', { name: 'Stop generation' })
|
||||
await expect(stopButton).toBeHidden({ timeout: 3000 })
|
||||
// Chat input should be available instead
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await expect(input).toBeVisible({ timeout: 3000 })
|
||||
})
|
||||
|
||||
test('web search toggle works', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
const toggle = page.getByRole('button', { name: 'Toggle web search' })
|
||||
await expect(toggle).toBeVisible({ timeout: 5000 })
|
||||
// Click to toggle web search on
|
||||
await toggle.click()
|
||||
// The button styling should change (it gains accent color when active)
|
||||
await expect(toggle).toBeVisible()
|
||||
// Click again to toggle off
|
||||
await toggle.click()
|
||||
await expect(toggle).toBeVisible()
|
||||
})
|
||||
|
||||
test('new conversation clears messages', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Click "New conversation" button
|
||||
await page.getByRole('button', { name: 'New conversation' }).click()
|
||||
// Previous messages should be cleared
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeHidden({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('panel side toggle switches layout', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 })
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open the panel
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
// Both chat and panel sections should be visible on desktop
|
||||
const chatSection = page.locator('section').first()
|
||||
await expect(chatSection).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { Conversation } from '@aiui/core/types/message'
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
/** Films: user asks for films, assistant responds with [[film:f1]] etc */
|
||||
export const filmsConversation: Conversation = {
|
||||
id: 'e2e-films',
|
||||
title: 'Film recommendations',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Recommend some sci-fi films',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are some great sci-fi films:\n\n- [[film:f1]] - Blade Runner 2049\n- [[film:f2]] - Arrival\n- [[film:f3]] - Dune\n\nAll from Denis Villeneuve.`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Magazine: news-like query + bullet sections (BIP/debate context) */
|
||||
export const magazineConversation: Conversation = {
|
||||
id: 'e2e-magazine',
|
||||
title: 'BIP 110 brief',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: "What's the latest on BIP 110? What are people saying?",
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `## Summary\n\nBIP 110 is being debated. Macro sentiment is bearish. BTC holding.\n\n- **Pro camp** — Technical improvement, faster.\n- **Anti camp** — Too risky, prefer status quo.\n\n**Henrik Zeberg** (analyst) says this could be bullish long-term.\n\nFor deeper analysis: check **Bitcoin Mailing List** (gnusha.org).`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Websites: user asks for resources, assistant gives markdown links */
|
||||
export const websitesConversation: Conversation = {
|
||||
id: 'e2e-websites',
|
||||
title: 'Bitcoin resources',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Best websites to check for Bitcoin news?',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are the best places to check:\n\n- [Bitcoin Magazine](https://bitcoinmagazine.com)\n- [Bitcoin.org](https://bitcoin.org)\n- [Mempool.space](https://mempool.space)`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** News: web search results + news-like response */
|
||||
export const newsConversation: Conversation = {
|
||||
id: 'e2e-news',
|
||||
title: 'Latest Bitcoin news',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: "What's the latest Bitcoin news?",
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here's what's happening. For the latest news check these sources:\n\n- [Bitcoin hits new high](https://example.com/btc-high)\n- [ETF inflows surge](https://example.com/etf-inflows)`,
|
||||
timestamp: now - 30000,
|
||||
webResults: [
|
||||
{ title: 'Bitcoin hits new high', url: 'https://example.com/btc-high', content: 'BTC reached...' },
|
||||
{ title: 'ETF inflows surge', url: 'https://example.com/etf-inflows', content: 'Spot ETF...' },
|
||||
],
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Songs: user asks for music, assistant responds with [[song:s1]] */
|
||||
export const songsConversation: Conversation = {
|
||||
id: 'e2e-songs',
|
||||
title: 'Music recommendations',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Recommend some math rock',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are great math rock tracks:\n\n- [[song:s1]] Never Meant by American Football\n- [[song:s2]] The Kill by Toe`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Podcasts */
|
||||
export const podcastsConversation: Conversation = {
|
||||
id: 'e2e-podcasts',
|
||||
title: 'Bitcoin podcasts',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Best Bitcoin podcasts?',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Check these:\n\n- [[podcast:p1]] What Bitcoin Did\n- [[podcast:p2]] The Audacity to Podcast`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
export const allTestConversations = {
|
||||
[filmsConversation.id]: filmsConversation,
|
||||
[magazineConversation.id]: magazineConversation,
|
||||
[websitesConversation.id]: websitesConversation,
|
||||
[newsConversation.id]: newsConversation,
|
||||
[songsConversation.id]: songsConversation,
|
||||
[podcastsConversation.id]: podcastsConversation,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { allTestConversations } from './fixtures/test-chats'
|
||||
|
||||
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
|
||||
|
||||
export default async function globalSetup() {
|
||||
const dir = resolve(process.cwd(), '.dev')
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
|
||||
// Backup existing chats if present (for local dev)
|
||||
let backup: string | null = null
|
||||
if (existsSync(CHATS_PATH)) {
|
||||
backup = readFileSync(CHATS_PATH, 'utf-8')
|
||||
}
|
||||
|
||||
const payload = {
|
||||
conversations: allTestConversations,
|
||||
activeConversationId: 'e2e-films',
|
||||
}
|
||||
writeFileSync(CHATS_PATH, JSON.stringify(payload, null, 2), 'utf-8')
|
||||
|
||||
// Store backup path for teardown (we pass via env since globalSetup/Teardown don't share scope easily)
|
||||
if (backup) {
|
||||
process.env.AIUI_E2E_CHATS_BACKUP = backup
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
|
||||
|
||||
export default async function globalTeardown() {
|
||||
const backup = process.env.AIUI_E2E_CHATS_BACKUP
|
||||
if (backup) {
|
||||
writeFileSync(CHATS_PATH, backup, 'utf-8')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('AIUI smoke tests', () => {
|
||||
test('app loads and shows chat interface', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveTitle(/AIUI/)
|
||||
})
|
||||
|
||||
test('chat input is visible and focusable', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const input = page.getByPlaceholder(/Message AIUI|Waiting for/)
|
||||
await expect(input).toBeVisible()
|
||||
})
|
||||
|
||||
test('content panel area exists', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const main = page.locator('main.path-glass-card')
|
||||
await expect(main).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Visual Regression', () => {
|
||||
test('ChatPage renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await expect(page).toHaveScreenshot('chat-page.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('ContentPanel renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// Open content panel by clicking a content tab
|
||||
const filmTab = page.getByRole('button', { name: /film/i }).first()
|
||||
if (await filmTab.isVisible()) {
|
||||
await filmTab.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot('content-panel-films.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('PassphraseDialog renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// PassphraseDialog shows on first load if crypto is enabled
|
||||
const dialog = page.locator('.glass-card').filter({ hasText: 'Unlock AIUI' })
|
||||
if (await dialog.isVisible()) {
|
||||
await expect(dialog).toHaveScreenshot('passphrase-dialog.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('BottomSheet renders correctly on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await expect(page).toHaveScreenshot('mobile-view.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user