diff --git a/frontend/e2e/helpers/subscriptions.ts b/frontend/e2e/helpers/subscriptions.ts index 955fcf7d..3a7a8cf6 100644 --- a/frontend/e2e/helpers/subscriptions.ts +++ b/frontend/e2e/helpers/subscriptions.ts @@ -2,7 +2,9 @@ import type { Locator, TestInfo } from '@playwright/test'; import { expect } from '@playwright/test'; -export const MESSAGE_ID_TESTID = 'flow-message-id'; +/** The message-id badge carries no accessible name, so it is addressed by its slot like every other + * shadcn primitive in this codebase — `data-testid` is not used in product code. */ +export const MESSAGE_ID_SELECTOR = '[data-slot="flow-message-id"]'; export const attachIdSet = async (testInfo: TestInfo, name: string, ids: string[]): Promise => { await testInfo.attach(name, { @@ -12,7 +14,7 @@ export const attachIdSet = async (testInfo: TestInfo, name: string, ids: string[ }; export const extractMessageIds = async (panel: Locator): Promise => { - const raw = await panel.getByTestId(MESSAGE_ID_TESTID).allTextContents(); + const raw = await panel.locator(MESSAGE_ID_SELECTOR).allTextContents(); return raw.map((text) => text.replace(/\D/g, '')).filter(Boolean); }; diff --git a/frontend/e2e/specs/cross/themes.spec.ts b/frontend/e2e/specs/cross/themes.spec.ts index c3e916d3..f9c9550e 100644 --- a/frontend/e2e/specs/cross/themes.spec.ts +++ b/frontend/e2e/specs/cross/themes.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '../../fixtures/test.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; -import { MESSAGE_ID_TESTID } from '../../helpers/subscriptions.ts'; +import { MESSAGE_ID_SELECTOR } from '../../helpers/subscriptions.ts'; import { flowsCassette } from '../../mocks/cassettes/flows.ts'; test.describe('themes', { tag: '@cross' }, () => { @@ -12,7 +12,7 @@ test.describe('themes', { tag: '@cross' }, () => { await page.getByRole('row', { name: /E2E Alpha/ }).click(); await expect(page.locator('html')).toHaveClass(/dark/); - await expect(page.getByTestId(MESSAGE_ID_TESTID).first()).toBeVisible(); + await expect(page.locator(MESSAGE_ID_SELECTOR).first()).toBeVisible(); await expect(page.locator('.xterm')).toBeVisible(); expectCleanPage(pageErrorLog); }); diff --git a/frontend/e2e/specs/flows/messages.spec.ts b/frontend/e2e/specs/flows/messages.spec.ts index 3d89a182..7b4c1643 100644 --- a/frontend/e2e/specs/flows/messages.spec.ts +++ b/frontend/e2e/specs/flows/messages.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '../../fixtures/test.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; +import { MESSAGE_ID_SELECTOR } from '../../helpers/subscriptions.ts'; import { variedMessagesCassette } from '../../mocks/cassettes/flows.ts'; type XtermHost = { @@ -36,7 +37,7 @@ test.describe('flow message rendering', { tag: '@flows' }, () => { pageErrorLog, }) => { await page.goto('/flows/5?tab=automation'); - await expect(page.getByTestId('flow-message-id')).toHaveCount(4); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(4); const thinkingToggle = page.getByText('Show thinking'); diff --git a/frontend/e2e/specs/flows/reconnect.spec.ts b/frontend/e2e/specs/flows/reconnect.spec.ts index 184b2308..d8b24650 100644 --- a/frontend/e2e/specs/flows/reconnect.spec.ts +++ b/frontend/e2e/specs/flows/reconnect.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '../../fixtures/test.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; import { dropAndReconnect } from '../../helpers/reconnect.ts'; -import { assertNoDuplicates, extractMessageIds, MESSAGE_ID_TESTID } from '../../helpers/subscriptions.ts'; +import { assertNoDuplicates, extractMessageIds, MESSAGE_ID_SELECTOR } from '../../helpers/subscriptions.ts'; import { FLOW_A_INITIAL_IDS, FLOW_A_RECONNECT_ID, @@ -32,7 +32,7 @@ test.describe('flow reconnect', { tag: ['@flows', '@smoke'] }, () => { await page.goto('/flows'); await page.getByRole('row', { name: /E2E Alpha/ }).click(); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(BEFORE_DROP.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(BEFORE_DROP.length); const loadsBeforeDrop = resourceLoads.length; @@ -42,11 +42,11 @@ test.describe('flow reconnect', { tag: ['@flows', '@smoke'] }, () => { .poll(() => resourceLoads.length, { message: 'the reconnect re-hydrates the REST resources slot' }) .toBe(loadsBeforeDrop + 1); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(AFTER_RECONNECT.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(AFTER_RECONNECT.length); world.raiseFlag(REPLAY_FLAG); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(AFTER_REPLAY.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(AFTER_REPLAY.length); const ids = await extractMessageIds(page.locator('body')); diff --git a/frontend/e2e/specs/flows/subscriptions.spec.ts b/frontend/e2e/specs/flows/subscriptions.spec.ts index 9bd0b2f0..723d5633 100644 --- a/frontend/e2e/specs/flows/subscriptions.spec.ts +++ b/frontend/e2e/specs/flows/subscriptions.spec.ts @@ -5,7 +5,7 @@ import { assertNoDuplicates, attachIdSet, extractMessageIds, - MESSAGE_ID_TESTID, + MESSAGE_ID_SELECTOR, } from '../../helpers/subscriptions.ts'; import { FLOW_A_INITIAL_IDS, @@ -29,7 +29,7 @@ test.describe('flow subscriptions', { tag: ['@flows', '@smoke'] }, () => { await page.goto('/flows'); await page.getByRole('row', { name: /E2E Alpha/ }).click(); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_A_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_A_IDS.length); const ids = await extractMessageIds(page.locator('body')); @@ -42,13 +42,13 @@ test.describe('flow subscriptions', { tag: ['@flows', '@smoke'] }, () => { test('keeps concurrent flows exact-set disjoint across a round trip', async ({ page, pageErrorLog }, testInfo) => { await page.goto('/flows'); await page.getByRole('row', { name: /E2E Alpha/ }).click(); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_A_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_A_IDS.length); const idsA = await extractMessageIds(page.locator('body')); await page.goBack(); await page.getByRole('row', { name: /E2E Beta/ }).click(); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_B_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_B_IDS.length); const idsB = await extractMessageIds(page.locator('body')); @@ -59,7 +59,7 @@ test.describe('flow subscriptions', { tag: ['@flows', '@smoke'] }, () => { await page.goBack(); await page.getByRole('row', { name: /E2E Alpha/ }).click(); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_A_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_A_IDS.length); const idsAgain = await extractMessageIds(page.locator('body')); @@ -81,14 +81,14 @@ test.describe('flow subscriptions across a pager switch', { tag: '@flows' }, () const streamB = subscriptionStreamKey('messageLogAdded', { flowId: '6' }); await page.goto('/flows/5'); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_A_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_A_IDS.length); expect(world.subscriberCount(streamA), 'flow A must be streaming before the switch').toBe(1); const idsA = await extractMessageIds(page.locator('body')); await page.locator('header').getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL(/\/flows\/6$/); - await expect(page.getByTestId(MESSAGE_ID_TESTID)).toHaveCount(FLOW_B_IDS.length); + await expect(page.locator(MESSAGE_ID_SELECTOR)).toHaveCount(FLOW_B_IDS.length); // The DOM cannot witness this leak: `messageLogs` is keyed by flowId, so a frame carrying // {flowId:'5'} is written to flow A's cache slot and never rendered under flow 6 however long @@ -98,7 +98,7 @@ test.describe('flow subscriptions across a pager switch', { tag: '@flows' }, () world.raiseFlag(PAGER_SWITCH_FLAG); - await expect(page.getByTestId(MESSAGE_ID_TESTID).filter({ hasText: FLOW_B_SENTINEL_ID })).toHaveCount(1); + await expect(page.locator(MESSAGE_ID_SELECTOR).filter({ hasText: FLOW_B_SENTINEL_ID })).toHaveCount(1); const idsB = await extractMessageIds(page.locator('body')); diff --git a/frontend/e2e/specs/real/flow-run.spec.ts b/frontend/e2e/specs/real/flow-run.spec.ts index 7bebd427..ad260c76 100644 --- a/frontend/e2e/specs/real/flow-run.spec.ts +++ b/frontend/e2e/specs/real/flow-run.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; +import { MESSAGE_ID_SELECTOR } from '../../helpers/subscriptions.ts'; import { readTerminalBuffer } from '../../helpers/terminal.ts'; test.describe('real backend flow run', { tag: '@real' }, () => { @@ -29,7 +30,7 @@ test.describe('real backend flow run', { tag: '@real' }, () => { // violation. const flowId = new URL(page.url()).pathname.match(/\d+/)?.[0] ?? ''; - await expect(page.getByTestId('flow-message-id').first()).toBeVisible({ timeout: 90_000 }); + await expect(page.locator(MESSAGE_ID_SELECTOR).first()).toBeVisible({ timeout: 90_000 }); await expect(page.getByText('Hello from the e2e mock LLM!').first()).toBeVisible({ timeout: 90_000 }); // The scenario's `uname -a` exec must stream its sandbox output back diff --git a/frontend/src/features/flows/messages/flow-message.tsx b/frontend/src/features/flows/messages/flow-message.tsx index 7f8cb0e3..b95bc2da 100644 --- a/frontend/src/features/flows/messages/flow-message.tsx +++ b/frontend/src/features/flows/messages/flow-message.tsx @@ -210,7 +210,7 @@ function FlowMessage({ log, searchValue = '' }: FlowMessageProps) { {formatDate(new Date(createdAt))} {log.id}