refactor(e2e): address the message id by slot, not by a test id

data-testid existed in exactly one place in product code — the message-id badge
this suite added — while every other primitive in the app is addressed through
the shadcn `data-slot` attribute. Switch it to `data-slot="flow-message-id"` and
move the five specs and the helper onto a selector constant. The remaining
data-testid occurrences are test-local stubs inside *.test.tsx files, not
product surface.

130 mock-tier tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-26 04:35:39 +07:00
co-authored by Claude Opus 4.8
parent a7b1533ceb
commit 7a86cb0a44
7 changed files with 23 additions and 19 deletions
+4 -2
View File
@@ -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<void> => {
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<string[]> => {
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);
};
+2 -2
View File
@@ -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);
});
+2 -1
View File
@@ -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');
+4 -4
View File
@@ -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'));
@@ -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'));
+2 -1
View File
@@ -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
@@ -210,7 +210,7 @@ function FlowMessage({ log, searchValue = '' }: FlowMessageProps) {
<span className="text-muted-foreground/50">{formatDate(new Date(createdAt))}</span>
<span
className="text-muted-foreground/50"
data-testid="flow-message-id"
data-slot="flow-message-id"
>
{log.id}
</span>