Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const Toast = ({ toast }: ToastProps) => {
'tw:animate-in tw:fade-in tw:slide-in-from-bottom-2 tw:duration-150'
)}
data-testid="alert-bar"
data-variant={variant}
toast={toast}>
<span className="tw:mt-0.5 tw:flex tw:shrink-0" data-testid="alert-icon">
<Icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
redirectToHomePage,
toastNotification,
uuid,
waitForToastToDisappear,
} from '../../../utils/common';
import {
dismissTagSuggestions,
Expand Down Expand Up @@ -319,10 +320,9 @@ test.describe(

await page.getByTestId('create-btn').click();
await updateTestCaseResponse;
await toastNotification(page, 'Test case updated successfully.');
await page.getByTestId('alert-bar').waitFor({
state: 'detached',
});
const updateSuccessMessage = 'Test case updated successfully.';
await toastNotification(page, updateSuccessMessage);
await waitForToastToDisappear(page, updateSuccessMessage);

await page
.getByTestId(`action-dropdown-${NEW_TABLE_TEST_CASE.name}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import test, { expect } from '@playwright/test';
import { SidebarItem } from '../../../constant/sidebar';
import { TableClass } from '../../../support/entity/TableClass';
import { UserClass } from '../../../support/user/UserClass';
import { createNewPage, redirectToHomePage } from '../../../utils/common';
import {
createNewPage,
expectNoErrorToast,
redirectToHomePage,
} from '../../../utils/common';
import { sidebarClick } from '../../../utils/sidebar';

type IncidentListResponse = {
Expand Down Expand Up @@ -193,10 +197,7 @@ test('Incident Manager renders after a test case owner change', async ({
const response = await pageIncidentListResponse;
expect(response.status()).toBe(200);

const errorToast = page
.locator('[data-testid="alert-bar"]')
.filter({ hasText: /Unrecognized field|owners/i });
await expect(errorToast).toHaveCount(0);
await expectNoErrorToast(page, /Unrecognized field|owners/i);
} finally {
await table.delete(apiContext).catch(() => undefined);
if (owner.responseData.id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
import test, { expect } from '@playwright/test';
import { SidebarItem } from '../../../constant/sidebar';
import { TableClass } from '../../../support/entity/TableClass';
import { createNewPage, redirectToHomePage } from '../../../utils/common';
import {
createNewPage,
expectNoErrorToast,
redirectToHomePage,
} from '../../../utils/common';
import { sidebarClick } from '../../../utils/sidebar';

test.use({ storageState: 'playwright/.auth/admin.json' });
Expand Down Expand Up @@ -105,12 +109,10 @@ test('Incident Manager renders without Jackson error after a test case is soft-d
expect(response.status()).toBe(200);

// And the toast bar must not surface a Jackson "Unrecognized field"/"deleted" error.
// Scope to the toast container so we don't false-positive on legitimate page text
// (e.g. a table cell that happens to contain the word "deleted").
const errorToast = page
.locator('[data-testid="alert-bar"]')
.filter({ hasText: /Unrecognized field|deleted/i });
await expect(errorToast).toHaveCount(0);
// Scoped to error-variant toasts so we don't false-positive on legitimate page text
// (e.g. a table cell containing "deleted") nor on the background
// '"<entity>" deleted successfully!' notification a parallel worker can trigger.
await expectNoErrorToast(page, /Unrecognized field|deleted/i);
} finally {
await table.delete(apiContext);
await afterAction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import { expect, test } from '@playwright/test';
import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config';
import { TableClass } from '../../support/entity/TableClass';
import { performAdminLogin } from '../../utils/admin';
import { getApiContext, redirectToHomePage, uuid } from '../../utils/common';
import {
expectNoErrorToast,
getApiContext,
redirectToHomePage,
uuid,
} from '../../utils/common';
import { fillDeleteConfirmationIfPresent } from '../../utils/entity';
import {
getFailedRowsData,
Expand Down Expand Up @@ -249,7 +254,7 @@ test.describe('Failed rows sample fetch gating', () => {

// The 404 is the expected "no sample stored" empty state — it must not
// surface as an error toast.
await expect(page.getByTestId('alert-bar')).not.toBeVisible();
await expectNoErrorToast(page);
});
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,49 @@ export const toastNotification = async (
await expect(toast.getByTestId('alert-icon')).toBeVisible();
};

/**
* Waits until the toast carrying `message` is gone.
*
* Always filter by message instead of waiting on a bare `alert-bar` locator: toasts
* are a stacking queue, and the backend fans async-delete/job notifications out to
* every socket of the logged-in user — so a parallel worker's cleanup can pop an
* unrelated toast into this page and turn an unfiltered locator into a strict-mode
* violation.
*/
export const waitForToastToDisappear = async (
page: Page,
message: string | RegExp,
timeout?: number
) => {
await page
.getByTestId('alert-bar')
.filter({ hasText: message })
.first()
.waitFor({ state: 'detached', timeout });
};

/**
* Asserts that the page is showing no error toast, optionally narrowed to the
* ones carrying `message`.
*
* Scoped to the error variant on purpose — a bare `alert-bar` assertion also
* catches the background success notifications the backend fans out to every
* socket of the logged-in user (async delete, export jobs), which a parallel
* worker can trigger at any moment.
*/
export const expectNoErrorToast = async (
page: Page,
message?: string | RegExp
) => {
const errorToast = page.locator(
'[data-testid="alert-bar"][data-variant="error"]'
);

await expect(
message ? errorToast.filter({ hasText: message }) : errorToast
).toHaveCount(0);
};

export const clickOutside = async (page: Page) => {
await page.locator('body').click({
position: {
Expand Down
Loading