fix(e2e): unambiguous chat conversation + new-channel locators — rc1-day2 root cause #1
22 @critical failures in 41-chat-deep.spec.ts shared one root cause:
`firstConversationRow` searched for `button[type="button"]` inside
the sidebar container, which also matched the "New Channel" CTA
button at the sidebar footer. When the listener test user had no
conversations seeded, `waitForConversationOrEmpty` raced and
returned 'has-conversations' because the CTA button matched the
conversation-row locator — `selectFirstConversation` then clicked
the CTA, opened CreateRoomDialog, and the subsequent
`expect(input).toBeEnabled()` failed because clicking the CTA
never set `currentConversationId`.
Fix:
* `data-testid="chat-conversation-item"` on ConversationItem
(+ `data-conversation-id` for callers that need the id).
* `data-testid="chat-new-channel-cta"` on the New Channel
footer button.
* `firstConversationRow` / `waitForConversationOrEmpty` /
`createRoom` rewired to target by testid. No more overlap.
* Shared helper `tests/e2e/helpers/conversation.ts` with a
minimal `navigateToConversation(page)` — picks the first
existing conversation if any, else creates a disposable one,
returns when the message input is enabled. Signature is
deliberately minimal (no options) to avoid the second-API-
surface trap. Future callers that need specialised behavior
set up store state directly instead of extending this helper.
Results:
* 22 failed → 20 passed / 3 failed / 10 skipped (graceful skips
when test user lacks seed data).
* The 3 remaining failures are distinct root causes:
- `:220` chat page debug text leak (suspected [object Object]
or undefined rendering somewhere in chat UI — real bug,
tracked separately)
- `:339` / `:347` createRoom DOM-detach race: the "Create
room" button gets detached mid-click, suggesting the dialog
is re-rendering during the click handler. Likely a fix in
the dialog lifecycle rather than the test. Tracked
separately.
29-chat-functional.spec.ts (2 failures on send-message) not
touched by this fix — those tests don't hit the row-vs-CTA
ambiguity, they fail further downstream when the backend doesn't
echo sent messages. Same class as #7 (backend-side chat
processing incomplete in test env).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5349b80052
commit
7c74a6d408
4 changed files with 88 additions and 17 deletions
|
|
@ -93,6 +93,7 @@ export const ChatSidebar: React.FC = () => {
|
|||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
className="w-full shadow-lg shadow-sm"
|
||||
variant="default"
|
||||
data-testid="chat-new-channel-cta"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Channel
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ function ConversationItemInner({
|
|||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onSelect(conversation.id)}
|
||||
data-testid="chat-conversation-item"
|
||||
data-conversation-id={conversation.id}
|
||||
className={cn(
|
||||
'appearance-none bg-transparent border-0 p-0 text-left w-full',
|
||||
'group relative flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all duration-[var(--sumi-duration-normal)] border border-transparent',
|
||||
|
|
|
|||
|
|
@ -51,16 +51,18 @@ async function waitForChatPageReady(page: Page): Promise<void> {
|
|||
).toBeVisible({ timeout: CONFIG.timeouts.navigation });
|
||||
}
|
||||
|
||||
/** Get the first conversation row from the sidebar (buttons rendered by ConversationItem). */
|
||||
/** Get the first conversation row from the sidebar (buttons rendered by ConversationItem).
|
||||
*
|
||||
* v1.0.7-rc1-day2: targeted by data-testid="chat-conversation-item"
|
||||
* (added on ConversationItem). The prior locator
|
||||
* `button[type="button"]` inside the sidebar also matched the
|
||||
* "New Channel" CTA button at the sidebar footer — producing the
|
||||
* 22-failure cascade where `selectFirstConversation` would click
|
||||
* the CTA, open CreateRoomDialog, and the test would fail at the
|
||||
* subsequent `expect(input).toBeEnabled()` because the click never
|
||||
* set currentConversationId. */
|
||||
function firstConversationRow(page: Page) {
|
||||
// ConversationItem renders a <button> whose left side contains the channel name
|
||||
// inside an h2 "Active Channels" sidebar. We narrow to that sidebar region.
|
||||
return page
|
||||
.locator('div.flex.flex-col.h-full')
|
||||
.filter({ has: page.locator('h2:has-text("Active Channels")') })
|
||||
.locator('button[type="button"]')
|
||||
.filter({ hasText: /.+/ })
|
||||
.first();
|
||||
return page.getByTestId('chat-conversation-item').first();
|
||||
}
|
||||
|
||||
/** Get the connection status indicator (dot next to "CHANNELS" header). */
|
||||
|
|
@ -89,13 +91,11 @@ async function waitForConversationOrEmpty(
|
|||
page: Page,
|
||||
timeout = 8_000,
|
||||
): Promise<'has-conversations' | 'empty' | 'unknown'> {
|
||||
const sidebar = page
|
||||
.locator('div.flex.flex-col.h-full')
|
||||
.filter({ has: page.locator('h2:has-text("Active Channels")') });
|
||||
|
||||
// Race between the first conversation row and the empty-state banner.
|
||||
const firstRow = sidebar.locator('button[type="button"]').first();
|
||||
const emptyBanner = sidebar.locator('text=/No conversations yet/i').first();
|
||||
// v1.0.7-rc1-day2: conversation rows targeted by testid instead of
|
||||
// the sidebar-region + button[type="button"] heuristic, which
|
||||
// matched the "New Channel" CTA alongside real conversations.
|
||||
const firstRow = page.getByTestId('chat-conversation-item').first();
|
||||
const emptyBanner = page.locator('text=/No conversations yet/i').first();
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
|
|
@ -128,7 +128,9 @@ async function selectFirstConversation(page: Page): Promise<boolean> {
|
|||
/** Create a brand-new channel via the sidebar dialog. Returns the name. */
|
||||
async function createRoom(page: Page): Promise<string> {
|
||||
const name = `e2e-deep-${Date.now()}`;
|
||||
await page.getByRole('button', { name: /new channel/i }).click();
|
||||
// v1.0.7-rc1-day2: target the sidebar CTA by testid (prior regex
|
||||
// was brittle and collided with the conversation-row locator).
|
||||
await page.getByTestId('chat-new-channel-cta').click();
|
||||
|
||||
const nameInput = page.locator('#room-name');
|
||||
await expect(nameInput).toBeVisible({ timeout: CONFIG.timeouts.action });
|
||||
|
|
|
|||
66
tests/e2e/helpers/conversation.ts
Normal file
66
tests/e2e/helpers/conversation.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Navigate to a chat conversation ready for message input.
|
||||
*
|
||||
* Minimal signature by design — a helper that starts growing options
|
||||
* becomes a second API surface. If a test needs a specific conversation
|
||||
* id, a specific room type, or pre-seeded messages, that test should
|
||||
* fetch via the API + set store state directly rather than extending
|
||||
* this helper. Keep it boring.
|
||||
*
|
||||
* Flow:
|
||||
* 1. /chat must already be loaded and the sidebar rendered.
|
||||
* 2. If at least one conversation row exists: click the first one.
|
||||
* 3. Otherwise: click "New Channel", create a disposable room.
|
||||
* 4. Wait for the message input to become enabled — that's the
|
||||
* observable signal that `currentConversationId` is set in the
|
||||
* chat store (ChatInput.tsx disables on `!currentConversationId`).
|
||||
*
|
||||
* Returns the conversation id chosen (via data-conversation-id attr)
|
||||
* when an existing one is selected, or null when a new one was
|
||||
* created (the id is assigned by the backend after dialog submit).
|
||||
*
|
||||
* Target selectors use testids added in v1.0.7-rc1-day2:
|
||||
* - chat-conversation-item (on ConversationItem button)
|
||||
* - chat-new-channel-cta (on the "New Channel" footer button)
|
||||
* The pre-rc1 tests used `button[type="button"]` which ambiguously
|
||||
* matched the "New Channel" CTA itself, producing the 22-failure
|
||||
* cascade.
|
||||
*/
|
||||
export async function navigateToConversation(
|
||||
page: Page,
|
||||
): Promise<string | null> {
|
||||
const existing = page.getByTestId('chat-conversation-item').first();
|
||||
const existingCount = await page.getByTestId('chat-conversation-item').count();
|
||||
|
||||
if (existingCount > 0) {
|
||||
const id = await existing.getAttribute('data-conversation-id');
|
||||
await existing.click();
|
||||
await expectInputReady(page);
|
||||
return id;
|
||||
}
|
||||
|
||||
// No conversation exists — create a disposable one. Room name is
|
||||
// timestamped to avoid collisions across concurrent test workers.
|
||||
const roomName = `e2e-helper-${Date.now()}`;
|
||||
await page.getByTestId('chat-new-channel-cta').click();
|
||||
|
||||
const nameInput = page.locator('#room-name');
|
||||
await expect(nameInput).toBeVisible({ timeout: 5_000 });
|
||||
await nameInput.fill(roomName);
|
||||
|
||||
await page.getByRole('button', { name: /^create room$/i }).click();
|
||||
|
||||
// Dialog closes, new conversation becomes current.
|
||||
await expect(nameInput).toBeHidden({ timeout: 5_000 });
|
||||
await expectInputReady(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function expectInputReady(page: Page): Promise<void> {
|
||||
const input = page.getByLabel('Type a message');
|
||||
await expect(input).toBeVisible({ timeout: 8_000 });
|
||||
await expect(input).toBeEnabled({ timeout: 8_000 });
|
||||
}
|
||||
Loading…
Reference in a new issue