diff --git a/README.md b/README.md index 5b01d48..7986c9a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ deliberate post detail link, or manually open a valid - Popular (`Top`) and chronological (`Latest`) search - Optional `filter:follows` search - Authenticated account list selection and list timelines from a deliberate URL or ID +- Relay profile switching from the profiles exposed by the relay - Infinite cursor pagination with explicit retry - Deliberate post detail pages with the visible conversation - Infinite conversation loading with explicit continuation retry @@ -45,9 +46,11 @@ cp .env.example .env.local The Nix development shell installs Hallmark's agent skill for the supported local agent targets. -Set `TWITTER_RELAY_BASE_URL` in `.env.local` for Vite development, and set -`BIRD_PROFILE_NAME` when the relay has multiple profiles. Export the same -values in the process environment before `start` or `test:live`: +Set `TWITTER_RELAY_BASE_URL` in `.env.local` for Vite development. +`BIRD_PROFILE_NAME` optionally selects the initial relay profile; the header +selector can switch between the profiles returned by the relay's `/profiles` +endpoint. Export the same values in the process environment before `start` or +`test:live`: ```bash export TWITTER_RELAY_BASE_URL=http://127.0.0.1:6900 diff --git a/e2e/integrations/reader.test.ts b/e2e/integrations/reader.test.ts index 2917333..fd013fa 100644 --- a/e2e/integrations/reader.test.ts +++ b/e2e/integrations/reader.test.ts @@ -43,6 +43,39 @@ test('requires intent and infinitely loads a user timeline', async ({ }) }) +test('switches the relay profile and reloads visible content', async ({ + homePage, + page, +}) => { + await homePage.goTo() + + const profile = page.getByLabel('プロファイル') + await expect(profile).toHaveValue('e2e') + await homePage.displayUser('@yuta') + await expect(page.getByText('user page 1')).toBeVisible() + + await profile.selectOption('alternate') + await expect(page.getByRole('status')).toHaveText('切り替えました。') + + await expect(page.getByText('alternate profile page 1')).toBeVisible() +}) + +test('keeps the profile selector within narrow viewports', async ({ + homePage, + page, +}) => { + for (const width of [320, 375, 414, 768]) { + await page.setViewportSize({ width, height: 900 }) + await homePage.goTo() + await expect(page.getByLabel('プロファイル')).toBeVisible() + expect( + await page.evaluate( + () => document.documentElement.scrollWidth > window.innerWidth, + ), + ).toBe(false) + } +}) + test('selects an authenticated list and infinitely loads its timeline', async ({ listsPage, page, @@ -152,6 +185,9 @@ test('keeps keyboard focus intentional and discovery or mutation controls absent }) => { await searchPage.goTo() + await searchPage.tabTo( + searchPage.page.getByRole('combobox', { name: 'プロファイル' }), + ) await searchPage.tabTo(searchPage.page.getByRole('link', { name: '検索' })) await searchPage.tabTo( searchPage.page.getByRole('link', { name: 'ユーザー' }), diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png index 9f685a2..12c918d 100644 Binary files a/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png and b/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png differ diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png index f9efcde..de2a180 100644 Binary files a/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png and b/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png differ diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png index dec2712..6e7ddb4 100644 Binary files a/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png and b/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png differ diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png index b2dfab8..217f5a8 100644 Binary files a/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png and b/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png differ diff --git a/e2e/mock-relay.mjs b/e2e/mock-relay.mjs index cf4fc93..e5b6e62 100644 --- a/e2e/mock-relay.mjs +++ b/e2e/mock-relay.mjs @@ -99,6 +99,11 @@ const timeline = (entries) => ({ instructions: [{ entries }] }) const server = createServer(async (request, response) => { const url = new URL(request.url ?? '/', `http://127.0.0.1:${port}`) + if (request.method === 'GET' && url.pathname === '/profiles') { + send(response, { profiles: ['e2e', 'alternate'] }) + return + } + const operation = [ 'UserByScreenName', 'UserTweets', @@ -120,7 +125,8 @@ const server = createServer(async (request, response) => { }) return } - if (request.headers['x-profile-name'] !== 'e2e') { + const profileName = request.headers['x-profile-name'] + if (profileName !== 'e2e' && profileName !== 'alternate') { fail(response, 403, 'invalid profile') return } @@ -166,10 +172,12 @@ const server = createServer(async (request, response) => { return } const secondPage = variables.cursor === 'user-next' + const firstPageText = + profileName === 'alternate' ? 'alternate profile page 1' : 'user page 1' const entries = secondPage ? [tweet('1010', 'user page 2')] : [ - tweet('1001', 'user page 1', 'yuta', { + tweet('1001', firstPageText, 'yuta', { conversationId: '1000', }), cursor('user-next'), diff --git a/src/components/app-shell.tsx b/src/components/app-shell.tsx index 8539ac6..e001633 100644 --- a/src/components/app-shell.tsx +++ b/src/components/app-shell.tsx @@ -1,4 +1,5 @@ import { Link } from '@tanstack/react-router' +import { ProfileSwitcher } from '#/features/profiles/profile-switcher' export function AppShell({ active, @@ -11,6 +12,7 @@ export function AppShell({

Twitter Lite

+