fix(web): stabilize invitation error assertions

The invitation error copy also renders as a transient sonner toast, so the
plain text locator resolved to two elements and tripped Playwright strict
mode while the toast was on screen.

Tag the inline error region with data-testid="invitation-error" and assert
against it, keeping the check deterministic under CI parallelism.
This commit is contained in:
TyperBody
2026-09-25 15:19:34 +08:00
parent 97c1addba7
commit 7543340f39
2 changed files with 13 additions and 9 deletions
+2 -2
View File
@@ -296,7 +296,7 @@ export default function AcceptInvitationPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-3 text-sm text-destructive"> <div className="flex items-start gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 size-4 shrink-0" /> <AlertCircle className="mt-0.5 size-4 shrink-0" />
<span>{errorMessage}</span> <span data-testid="invitation-error">{errorMessage}</span>
</div> </div>
<Button <Button
variant="outline" variant="outline"
@@ -313,7 +313,7 @@ export default function AcceptInvitationPage() {
{errorMessage && ( {errorMessage && (
<div className="flex items-start gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-3 text-sm text-destructive"> <div className="flex items-start gap-2 rounded-lg border border-destructive/20 bg-destructive/5 p-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 size-4 shrink-0" /> <AlertCircle className="mt-0.5 size-4 shrink-0" />
<span>{errorMessage}</span> <span data-testid="invitation-error">{errorMessage}</span>
</div> </div>
)} )}
+11 -7
View File
@@ -27,9 +27,9 @@ test('terminal invitation errors refresh on a new fragment and allow account swi
}); });
await page.goto('/invitations/accept#token=used-invitation'); await page.goto('/invitations/accept#token=used-invitation');
await expect( await expect(page.getByTestId('invitation-error')).toContainText(
page.getByText('This invitation was already used.'), 'This invitation was already used.',
).toBeVisible(); );
await expect await expect
.poll(() => .poll(() =>
page.evaluate(() => page.evaluate(() =>
@@ -41,7 +41,9 @@ test('terminal invitation errors refresh on a new fragment and allow account swi
await page.evaluate(() => { await page.evaluate(() => {
window.location.hash = 'token=revoked-invitation'; window.location.hash = 'token=revoked-invitation';
}); });
await expect(page.getByText('This invitation was revoked.')).toBeVisible(); await expect(page.getByTestId('invitation-error')).toContainText(
'This invitation was revoked.',
);
await page.getByRole('button', { name: 'Back to sign in' }).click(); await page.getByRole('button', { name: 'Back to sign in' }).click();
await expect(page).toHaveURL(/\/login$/); await expect(page).toHaveURL(/\/login$/);
@@ -101,9 +103,11 @@ test('login preserves an explicit invitation email mismatch error', async ({
await expect(page).toHaveURL( await expect(page).toHaveURL(
/\/invitations\/accept\?error=invitation_email_mismatch$/, /\/invitations\/accept\?error=invitation_email_mismatch$/,
); );
await expect( // The same copy is also shown as a transient toast, so target the page's
page.getByText('This invitation belongs to a different email address.'), // inline error region to keep the assertion deterministic.
).toBeVisible(); await expect(page.getByTestId('invitation-error')).toContainText(
'This invitation belongs to a different email address.',
);
await expect(page.getByText('Login successful')).toHaveCount(0); await expect(page.getByText('Login successful')).toHaveCount(0);
}); });