feat: migrate workspace to shadcn sidebar and chat components
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { expect, test } from '../fixtures'
|
||||
import { showDeckManagement } from '../workspace'
|
||||
|
||||
test('opens account management after hydration without inheriting real OAuth configuration', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
await showDeckManagement(page)
|
||||
const manage = page.getByRole('button', { name: '接続アカウントを管理' })
|
||||
await expect(manage).toBeEnabled()
|
||||
await manage.click()
|
||||
@@ -28,6 +30,7 @@ test('rejects an unbound OAuth callback without contacting an instance', async (
|
||||
await expect(page.getByRole('alert')).toContainText(
|
||||
'Mastodonの認可を完了できませんでした。',
|
||||
)
|
||||
await showDeckManagement(page)
|
||||
await expect(
|
||||
page.getByRole('button', { name: '接続アカウントを管理' }),
|
||||
).toBeEnabled()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect, test } from '../fixtures'
|
||||
import { closeMobileSidebar, showDeckManagement } from '../workspace'
|
||||
|
||||
async function addColumn(
|
||||
page: Page,
|
||||
@@ -8,6 +9,7 @@ async function addColumn(
|
||||
profile = 'e2e',
|
||||
kind = 'search',
|
||||
) {
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'カラムを追加', exact: true }).click()
|
||||
const editor = page.getByRole('form', { name: 'カラムを追加', exact: true })
|
||||
await editor.getByLabel('カラム名', { exact: true }).fill(title)
|
||||
@@ -28,6 +30,7 @@ async function addColumn(
|
||||
)
|
||||
.fill(query)
|
||||
await editor.getByRole('button', { name: '追加して検索' }).click()
|
||||
await closeMobileSidebar(page)
|
||||
await expect(
|
||||
page
|
||||
.getByRole('region', { name: title, exact: true })
|
||||
@@ -44,7 +47,9 @@ test.beforeEach(async ({ page }) => {
|
||||
await page
|
||||
.getByRole('button', { name: 'デッキとして保存', exact: true })
|
||||
.click()
|
||||
await expect(page.getByText('サーバーに保存・端末間で共有')).toHaveCount(1)
|
||||
await showDeckManagement(page)
|
||||
await expect(page.getByText('サーバーに保存・端末間で共有')).toBeVisible()
|
||||
await closeMobileSidebar(page)
|
||||
})
|
||||
|
||||
test('binds identical searches to independent profiles through paging, editing and reload', async ({
|
||||
@@ -67,8 +72,8 @@ test('binds identical searches to independent profiles through paging, editing a
|
||||
await expect(
|
||||
second.getByText('alternate · Latest · all page 2'),
|
||||
).toBeVisible()
|
||||
await second.locator('summary[aria-label="別の接続の操作"]').click()
|
||||
await second.getByRole('button', { name: '編集', exact: true }).click()
|
||||
await second.getByRole('button', { name: '別の接続の操作' }).click()
|
||||
await page.getByRole('menuitem', { name: '編集', exact: true }).click()
|
||||
await page
|
||||
.getByRole('combobox', { name: '接続プロファイル', exact: true })
|
||||
.selectOption({ label: 'e2e' })
|
||||
@@ -93,25 +98,32 @@ test('creates, switches, renames and deletes persisted deck profiles', async ({
|
||||
await page.getByLabel('調査テーマ・デッキ名').fill('最初の調査')
|
||||
await page.getByRole('button', { name: '名前を保存' }).click()
|
||||
await addColumn(page, '最初の観点', 'first-deck')
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'デッキを作成', exact: true }).click()
|
||||
await page.getByLabel('新しいデッキ名').fill('別の調査')
|
||||
await page.getByRole('button', { name: '作成', exact: true }).click()
|
||||
await expect(page.locator('.deck-column')).toHaveCount(0)
|
||||
await addColumn(page, '別の観点', 'second-deck', 'alternate')
|
||||
await showDeckManagement(page)
|
||||
await page
|
||||
.getByLabel('デッキプロファイル', { exact: true })
|
||||
.selectOption({ label: '最初の調査' })
|
||||
await closeMobileSidebar(page)
|
||||
await expect(page.locator('.deck-column h2')).toHaveText(['最初の観点'])
|
||||
await page.reload()
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査')
|
||||
await showDeckManagement(page)
|
||||
await page
|
||||
.getByLabel('デッキプロファイル', { exact: true })
|
||||
.selectOption({ label: '別の調査' })
|
||||
await closeMobileSidebar(page)
|
||||
await expect(page.locator('.deck-column h2')).toHaveText(['別の観点'])
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'デッキを削除', exact: true }).click()
|
||||
await page.getByRole('button', { name: '削除する', exact: true }).click()
|
||||
await page.reload()
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査')
|
||||
await showDeckManagement(page)
|
||||
await expect(
|
||||
page.getByLabel('デッキプロファイル', { exact: true }).locator('option'),
|
||||
).toHaveCount(1)
|
||||
@@ -128,14 +140,14 @@ test('reads user and list timelines within columns and reorders, deletes and res
|
||||
await expect(list.getByText('list page 1')).toBeVisible()
|
||||
await list.getByRole('button', { name: '続きを読み込む' }).click()
|
||||
await expect(list.getByText('list page 2')).toBeVisible()
|
||||
await list.locator('summary[aria-label="リストの投稿の操作"]').click()
|
||||
await list.getByRole('button', { name: 'リストの投稿を左へ' }).click()
|
||||
await list.getByRole('button', { name: 'リストの投稿の操作' }).click()
|
||||
await page.getByRole('menuitem', { name: 'リストの投稿を左へ' }).click()
|
||||
await expect(page.locator('.deck-column h2')).toHaveText([
|
||||
'リストの投稿',
|
||||
'アカウント',
|
||||
])
|
||||
await list.locator('summary[aria-label="リストの投稿の操作"]').click()
|
||||
await list.getByRole('button', { name: 'リストの投稿を削除' }).click()
|
||||
await list.getByRole('button', { name: 'リストの投稿の操作' }).click()
|
||||
await page.getByRole('menuitem', { name: 'リストの投稿を削除' }).click()
|
||||
await page.getByRole('button', { name: '元に戻す' }).click()
|
||||
await expect(page.locator('.deck-column h2')).toHaveText([
|
||||
'リストの投稿',
|
||||
@@ -200,6 +212,7 @@ test('keeps the deck and editor accessible within narrow viewports', async ({
|
||||
path: testInfo.outputPath('deck-populated.png'),
|
||||
fullPage: true,
|
||||
})
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'カラムを追加', exact: true }).click()
|
||||
expect((await a11y().analyze()).violations).toEqual([])
|
||||
await page.screenshot({
|
||||
@@ -211,12 +224,17 @@ test('keeps the deck and editor accessible within narrow viewports', async ({
|
||||
test('closes the column editor with Escape and restores focus without applying edits', async ({
|
||||
page,
|
||||
}) => {
|
||||
await showDeckManagement(page)
|
||||
const opener = page.getByRole('button', { name: 'カラムを追加', exact: true })
|
||||
await opener.click()
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('dialog', { name: 'カラムを追加', exact: true }),
|
||||
).toBeVisible()
|
||||
await page.getByLabel('カラム名', { exact: true }).fill('キャンセルする観点')
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0)
|
||||
await expect(
|
||||
page.getByRole('dialog', { name: 'カラムを追加', exact: true }),
|
||||
).toHaveCount(0)
|
||||
await expect(opener).toBeFocused()
|
||||
await expect(page.locator('.deck-column')).toHaveCount(0)
|
||||
await opener.click()
|
||||
@@ -239,6 +257,7 @@ test('does not overwrite unreadable storage on reload', async ({ page }) => {
|
||||
test('selects a list from the column profile catalog and refreshes it on profile change', async ({
|
||||
page,
|
||||
}) => {
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'カラムを追加', exact: true }).click()
|
||||
await page.getByLabel('カラム名', { exact: true }).fill('購読リスト')
|
||||
await page
|
||||
@@ -263,6 +282,7 @@ test('selects a list from the column profile catalog and refreshes it on profile
|
||||
'1234567890',
|
||||
)
|
||||
await page.getByRole('button', { name: '追加して検索' }).click()
|
||||
await closeMobileSidebar(page)
|
||||
await expect(
|
||||
page
|
||||
.getByRole('region', { name: '購読リスト', exact: true })
|
||||
@@ -347,11 +367,15 @@ test('imports old browser decks explicitly and avoids duplicate imports after re
|
||||
legacy,
|
||||
)
|
||||
await page.reload()
|
||||
await showDeckManagement(page)
|
||||
const selector = page.getByLabel('デッキプロファイル', { exact: true })
|
||||
await expect(selector.locator('option')).toHaveCount(1)
|
||||
await closeMobileSidebar(page)
|
||||
await page.getByRole('button', { name: '旧デッキを取り込む' }).click()
|
||||
await showDeckManagement(page)
|
||||
await expect(selector.locator('option')).toHaveCount(2)
|
||||
await selector.selectOption({ label: '以前の調査' })
|
||||
await closeMobileSidebar(page)
|
||||
await expect(
|
||||
page.getByText('Latest · all page 1', { exact: true }),
|
||||
).toBeVisible()
|
||||
@@ -368,5 +392,29 @@ test('imports old browser decks explicitly and avoids duplicate imports after re
|
||||
await expect(
|
||||
page.getByRole('button', { name: '旧デッキを取り込む' }),
|
||||
).toHaveCount(0)
|
||||
await showDeckManagement(page)
|
||||
await expect(selector.locator('option')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('preserves a new deck dialog and its draft across the mobile breakpoint', async ({
|
||||
page,
|
||||
}) => {
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'デッキを作成', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'デッキを作成', exact: true })
|
||||
await dialog.getByLabel('新しいデッキ名').fill('画面を回転しても残る調査')
|
||||
for (const width of [1024, 375, 1024]) {
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
await expect(dialog).toHaveCount(1)
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByLabel('新しいデッキ名')).toHaveValue(
|
||||
'画面を回転しても残る調査',
|
||||
)
|
||||
await expect(page.locator('#new-deck-title')).toHaveCount(1)
|
||||
}
|
||||
await dialog.getByRole('button', { name: '作成', exact: true }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
|
||||
'画面を回転しても残る調査',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { DeckColumn } from '../../src/features/decks/model'
|
||||
import type { ResearchRun } from '../../src/features/research/model'
|
||||
import { expect, test } from '../fixtures'
|
||||
import {
|
||||
closeMobileSidebar,
|
||||
showDeckManagement,
|
||||
showResearch,
|
||||
} from '../workspace'
|
||||
|
||||
test('pushes the current research snapshot on each SSE connection', async ({
|
||||
page,
|
||||
@@ -37,6 +42,7 @@ test('restores the conversation and growing temporary deck from reconnect snapsh
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'カラムを追加', exact: true }).click()
|
||||
const accounts = page.getByRole('combobox', {
|
||||
name: '接続プロファイル',
|
||||
@@ -45,9 +51,11 @@ test('restores the conversation and growing temporary deck from reconnect snapsh
|
||||
await accounts.selectOption({ label: 'e2e' })
|
||||
const connectionId = await accounts.inputValue()
|
||||
await page.keyboard.press('Escape')
|
||||
await closeMobileSidebar(page)
|
||||
const run: ResearchRun = {
|
||||
id: '36ad8cc7-318c-46ae-94fa-28d30336f027',
|
||||
threadId: 'conversation-test',
|
||||
connectionIds: [connectionId],
|
||||
topic: '既存の観点を使って調査して',
|
||||
status: 'running',
|
||||
startedAt: Date.now(),
|
||||
@@ -89,10 +97,14 @@ test('restores the conversation and growing temporary deck from reconnect snapsh
|
||||
}),
|
||||
)
|
||||
await page.reload()
|
||||
await showResearch(page)
|
||||
const conversation = page.getByRole('log', { name: '調査の会話' })
|
||||
await expect(conversation).toContainText('既存の観点を使って調査して')
|
||||
await expect(conversation).toContainText('list_decks: 完了')
|
||||
await expect(conversation).toContainText('既存の検索条件を再利用しています。')
|
||||
await expect(
|
||||
conversation.locator('[data-slot="bubble-content"]'),
|
||||
).not.toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
|
||||
await expect(page.locator('.deck-column h2')).toHaveText(['最初の観点'])
|
||||
run.deckVersion = 2
|
||||
run.status = 'complete'
|
||||
@@ -114,19 +126,79 @@ test('restores the conversation and growing temporary deck from reconnect snapsh
|
||||
},
|
||||
})
|
||||
await page.reload()
|
||||
await showResearch(page)
|
||||
await expect(conversation).toContainText('既存の観点を使って調査して')
|
||||
await expect(conversation).toContainText('比較する観点を追加しました。')
|
||||
await expect(page.locator('.deck-column h2')).toHaveText([
|
||||
'最初の観点',
|
||||
'比較の観点',
|
||||
])
|
||||
await closeMobileSidebar(page)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'デッキとして保存', exact: true }),
|
||||
).toBeEnabled()
|
||||
await showResearch(page)
|
||||
const accountPicker = page.getByRole('button', {
|
||||
name: '接続アカウント · 1件',
|
||||
})
|
||||
await accountPicker.click()
|
||||
await expect(accountPicker).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'e2e · Twitter' }),
|
||||
).toBeChecked()
|
||||
await accountPicker.click()
|
||||
await expect(accountPicker).toHaveAttribute('aria-expanded', 'false')
|
||||
await page.getByLabel('メッセージ', { exact: true }).fill('この観点を詳しく')
|
||||
await expect(
|
||||
page.getByRole('button', { name: '送信', exact: true }),
|
||||
).toBeEnabled()
|
||||
await page
|
||||
.getByRole('button', { name: 'サイドバーを閉じる', exact: true })
|
||||
.click()
|
||||
run.deckVersion = 3
|
||||
if (run.deck) run.deck.title = '閉じている間に更新された調査'
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
|
||||
'閉じている間に更新された調査',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
await showResearch(page)
|
||||
await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue(
|
||||
'この観点を詳しく',
|
||||
)
|
||||
await page.setViewportSize({ width: 600, height: 900 })
|
||||
await expect(
|
||||
page.locator('[data-sidebar="sidebar"][data-mobile="true"]'),
|
||||
).toHaveCount(1)
|
||||
await showResearch(page)
|
||||
await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue(
|
||||
'この観点を詳しく',
|
||||
)
|
||||
await page.getByRole('button', { name: '接続アカウント · 1件' }).click()
|
||||
await page.getByRole('checkbox', { name: 'e2e · Twitter' }).uncheck()
|
||||
await page.setViewportSize({ width: 1100, height: 900 })
|
||||
await expect(
|
||||
page.locator('[data-sidebar="sidebar"][data-mobile="true"]'),
|
||||
).toHaveCount(0)
|
||||
await showResearch(page)
|
||||
await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue(
|
||||
'この観点を詳しく',
|
||||
)
|
||||
await page.getByRole('button', { name: '接続アカウント · 0件' }).click()
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'e2e · Twitter' }),
|
||||
).not.toBeChecked()
|
||||
await page.setViewportSize({ width: 600, height: 900 })
|
||||
await expect(
|
||||
page.locator('[data-sidebar="sidebar"][data-mobile="true"]'),
|
||||
).toHaveCount(1)
|
||||
await showResearch(page)
|
||||
await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue(
|
||||
'この観点を詳しく',
|
||||
)
|
||||
await page.getByRole('button', { name: '接続アカウント · 0件' }).click()
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'e2e · Twitter' }),
|
||||
).not.toBeChecked()
|
||||
})
|
||||
|
||||
test('rejects a different tailnet owner at the SSE endpoint', async ({
|
||||
@@ -144,6 +216,7 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col
|
||||
}) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
await page.goto('/')
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'カラムを追加', exact: true }).click()
|
||||
const accounts = page.getByRole('combobox', {
|
||||
name: '接続プロファイル',
|
||||
@@ -152,6 +225,7 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col
|
||||
await accounts.selectOption({ label: 'e2e' })
|
||||
const connectionId = await accounts.inputValue()
|
||||
await page.keyboard.press('Escape')
|
||||
await closeMobileSidebar(page)
|
||||
const source: DeckColumn = {
|
||||
id: 'citation-column-2',
|
||||
title: '引用の観点',
|
||||
@@ -213,8 +287,14 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col
|
||||
}),
|
||||
)
|
||||
await page.reload()
|
||||
await showResearch(page)
|
||||
const chat = page.getByRole('log', { name: '調査の会話' })
|
||||
await expect(chat.getByRole('heading', { name: '調査結果' })).toBeVisible()
|
||||
await expect(chat.getByRole('heading', { name: '調査結果' })).toHaveCSS(
|
||||
'font-weight',
|
||||
'600',
|
||||
)
|
||||
await expect(chat.locator('ul')).toHaveCSS('list-style-type', 'disc')
|
||||
await expect(
|
||||
chat.locator('strong').filter({ hasText: '重要な発見' }),
|
||||
).toBeVisible()
|
||||
@@ -246,6 +326,7 @@ test('explains unconfigured Codex and prevents starting research without affecti
|
||||
const heading = page.getByRole('heading', { level: 1 })
|
||||
await expect(heading).toBeVisible()
|
||||
const originalTitle = await heading.textContent()
|
||||
await showResearch(page)
|
||||
const panel = page.getByRole('complementary', { name: '調査チャット' })
|
||||
await expect(panel).toBeVisible()
|
||||
await expect(
|
||||
@@ -257,10 +338,14 @@ test('explains unconfigured Codex and prevents starting research without affecti
|
||||
await expect(panel.getByLabel('メッセージ', { exact: true })).toBeDisabled()
|
||||
const accessibility = await a11y().analyze()
|
||||
expect(accessibility.violations).toEqual([])
|
||||
await closeMobileSidebar(page)
|
||||
await expect(heading).toHaveText(originalTitle ?? '')
|
||||
await showDeckManagement(page)
|
||||
await page.getByRole('button', { name: 'デッキを作成', exact: true }).click()
|
||||
await page.getByLabel('新しいデッキ名').fill('手動の調査')
|
||||
await page.getByRole('button', { name: '作成', exact: true }).click()
|
||||
await closeMobileSidebar(page)
|
||||
await expect(heading).toHaveText('手動の調査')
|
||||
await showResearch(page)
|
||||
await expect(panel).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -117,12 +117,9 @@ test('creates temporary research, edits it, persists explicitly and reopens it o
|
||||
).toBeVisible()
|
||||
await page
|
||||
.getByRole('region', { name: '日本語', exact: true })
|
||||
.locator('summary')
|
||||
.click()
|
||||
await page
|
||||
.getByRole('region', { name: '日本語', exact: true })
|
||||
.getByRole('button', { name: '編集', exact: true })
|
||||
.getByRole('button', { name: '日本語の操作' })
|
||||
.click()
|
||||
await page.getByRole('menuitem', { name: '編集', exact: true }).click()
|
||||
const replaced = await executeTool(page, 'set_deck', {
|
||||
deckId: deck.id,
|
||||
title: '更新した調査',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from '../fixtures'
|
||||
import { closeMobileSidebar, showResearch } from '../workspace'
|
||||
|
||||
test('keeps the sidebar, composer and deck reachable at compact widths', async ({
|
||||
page,
|
||||
a11y,
|
||||
}, testInfo) => {
|
||||
await page.goto('/')
|
||||
for (const width of [320, 375, 414, 768]) {
|
||||
await page.setViewportSize({ width, height: 850 })
|
||||
await showResearch(page)
|
||||
const chat = page.getByRole('complementary', { name: '調査チャット' })
|
||||
await expect(chat).toBeVisible()
|
||||
const composer = chat.getByRole('textbox', {
|
||||
name: 'メッセージ',
|
||||
exact: true,
|
||||
})
|
||||
await expect(composer).toBeVisible()
|
||||
const bounds = await composer.boundingBox()
|
||||
if (!bounds) throw new Error('Composer must have a visible bounding box')
|
||||
expect(bounds.x).toBeGreaterThanOrEqual(0)
|
||||
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width)
|
||||
expect(bounds.y + bounds.height).toBeLessThanOrEqual(850)
|
||||
expect(
|
||||
await page.evaluate(() => document.documentElement.scrollWidth),
|
||||
).toBe(width)
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath(`workspace-${width}.png`),
|
||||
})
|
||||
if (width === 320) expect((await a11y().analyze()).violations).toEqual([])
|
||||
await closeMobileSidebar(page)
|
||||
}
|
||||
await page.getByRole('button', { name: 'サイドバーを切り替え' }).click()
|
||||
await expect(
|
||||
page.getByRole('complementary', { name: '調査チャット' }),
|
||||
).toBeHidden()
|
||||
await page.getByRole('button', { name: '調査チャット', exact: true }).click()
|
||||
await expect(
|
||||
page.getByRole('complementary', { name: '調査チャット' }),
|
||||
).toBeVisible()
|
||||
})
|
||||
Reference in New Issue
Block a user