Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"concurrently": "9.1.0",
"husky": "^9.1.6",
"typescript": "^5.7.3",
"vitest": "^3.2.4",
"@vitest/ui": "^3.2.4",
"vitest": "^4.0.10",
"@vitest/ui": "^4.0.10",
"lint-staged": "^15.4.3",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/api-v4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"@linode/tsconfig": "workspace:*",
"axios-mock-adapter": "^1.22.0",
"concurrently": "^9.0.1",
"tsup": "^8.4.0"
"tsup": "^8.4.0",
"@types/node": "^22.13.14"
},
"lint-staged": {
"*.{ts,tsx,js}": [
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
"pdfreader": "^3.0.7",
"redux-mock-store": "^1.5.3",
"storybook": "^9.0.12",
"vite": "^7.1.11",
"vite": "^7.2.2",
"vite-plugin-svgr": "^4.5.0"
},
"browserslist": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { transformDimensionValue } from '../Utils/utils';
import { transformDimensionValue } from '../CreateAlert/Criteria/DimensionFilterValue/utils';
import {
LINODE_DIMENSION_LABEL,
NODEBALANCER_DIMENSION_LABEL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ import {
} from 'src/queries/cloudpulse/useAlertsMutation';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';

import { useContextualAlertsState } from '../../Utils/utils';
import { arraysEqual, useContextualAlertsState } from '../../Utils/utils';
import { AlertConfirmationDialog } from '../AlertsLanding/AlertConfirmationDialog';
import { ALERT_SCOPE_TOOLTIP_CONTEXTUAL } from '../constants';
import { scrollToElement } from '../Utils/AlertResourceUtils';
import { arraysEqual } from '../Utils/utils';
import { AlertInformationActionRow } from './AlertInformationActionRow';

import type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ describe('AlertDefinition Create', () => {

it(
'should render client side validation errors for threshold and trigger occurences text field',
{ timeout: 10000 },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bnussman-akamai - reliance on timeouts can only lead to more flaky tests. Is there any better workaround? like awaiting?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may be able to drop the explicit timeout on some of the test but I'm keeping them for now to try to minimize any new flake. Vitest v4 already seems to be posing some new flake that I don't know exactly how to solve right now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abailly-akamai @bnussman-akamai I haven't looked at this particular case, but TBH I think a lot of our unit test CI flake is caused by performance issues stemming from JSDom + the parallelization of our tests where increasing the timeout really is the only solution in reach.

I'm only speculating because I don't know how to actually prove it, but here's an explanation of my reasoning that I shared with @HanaXu in Slack a few weeks back when she encountered a similar seemingly random unit test failure (#13015):

I'm not sure exactly what's going on, but usually when this happens (i.e. a unit test starts failing with a timeout in CI that passes reliably locally) increasing the test/assertion* timeout fixes it. I have a feeling it will here too.

I'm speculating, but I think when we run the tests in parallel on CI hardware, JSDom -- which is pretty slow and demanding -- is actually struggling to render some of our components before the Vitest assertions time out, and I think these failures can be triggered by seemingly random/unrelated changes to other tests that just so happen to impact the order/timing of test execution in a way that causes multiple particularly demanding tests to run at the same time when before they didn't, leading to these random but persistent timeouts.

One reason I think this is the case is that back when I was still on my Intel MacBook, I used to be able to reproduce these types of failures locally pretty consistently by running multiple specs at a time whereas running only the failing spec would tend to pass. Since upgrading to the M3, I haven't been able to reproduce any of these types of failures, so I'm pretty confident that performance is a factor in some way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - this makes sense to me. I am less worried about these few instances than I am about proliferation of those. Could be overused by contributors once it starts spreading. But maybe that's a moot concern at this point

Copy link
Copy Markdown
Member Author

@bnussman-akamai bnussman-akamai Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much acknowledge and agree with the observations on the performance issues. I experienced that first hand with my Intel Mac and we're seeing it on Akamai's internal CI runners. πŸ˜…

In the case of this PR, while the performance issues are likely a factor in play, I think there is more to the story. I'm seeing new flake that I haven't seen before and I'm speculating it's a symptom of Vitests new pool/threading implementation that was introduced in v4

To address this new flake, I seem to be having some success with

  • Ensuring Promises like userEvent.click() are awaited
  • Mocking flags using our renderWithTheme wrapper options rather than module mocking
  • Fixing some circular imports (primarily in CloudPulse code)

async () => {
const user = userEvent.setup();
const container = renderWithTheme(<CreateAlertDefinition />);
Expand Down Expand Up @@ -173,8 +174,7 @@ describe('AlertDefinition Create', () => {
expect(
container.getAllByText('The value should be a number.').length
).toBe(2);
},
{ timeout: 10000 }
}
);

it('should render the client side validation error messages for the form', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { linodeFactory, nodeBalancerFactory } from '@linode/utilities';

import { transformDimensionValue } from '../../../Utils/utils';
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to move around numerous CloudPulse functions to avoid circular imports that were causing tests to intermittently fail.

No logic was changed, I just had to move things around.

import {
getFilteredFirewallParentEntities,
getFirewallLinodes,
Expand All @@ -11,6 +10,7 @@ import {
handleValueChange,
resolveSelectedValues,
scopeBasedFilteredResources,
transformDimensionValue,
} from './utils';

import type { Linode } from '@linode/api-v4';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { transformDimensionValue } from '../../../Utils/utils';
import {
DIMENSION_TRANSFORM_CONFIG,
TRANSFORMS,
} from 'src/features/CloudPulse/shared/DimensionTransform';

import type { Item } from '../../../constants';
import type { OperatorGroup } from './constants';
Expand All @@ -13,6 +16,25 @@ import type {
import type { CloudPulseResources } from 'src/features/CloudPulse/shared/CloudPulseResourcesSelect';
import type { FirewallEntity } from 'src/features/CloudPulse/shared/types';

/**
* Transform a dimension value using the appropriate transform function
* @param serviceType - The cloud pulse service type
* @param dimensionLabel - The dimension label
* @param value - The value to transform
* @returns Transformed value
*/
export const transformDimensionValue = (
serviceType: CloudPulseServiceType | null,
dimensionLabel: string,
value: string
): string => {
return (
(
serviceType && DIMENSION_TRANSFORM_CONFIG[serviceType]?.[dimensionLabel]
)?.(value) ?? TRANSFORMS.capitalize(value)
);
};

/**
* Resolves the selected value(s) for the Autocomplete component from raw string.
* @param options - List of selectable options.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ const pollingIntervalOptions =
}));

describe('Trigger Conditions', () => {
const user = userEvent.setup();

it('should render all the components and names', () => {
renderWithThemeAndHookFormContext({
component: (
Expand Down Expand Up @@ -120,7 +118,7 @@ describe('Trigger Conditions', () => {
{ name: 'Open' }
);

user.click(evaluationPeriodInput);
await userEvent.click(evaluationPeriodInput);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not awaiting some of these userEvent calls was causing some failures/flake. The fix was to just ensure userEvent calls were awaited.


expect(
await screen.findByRole('option', {
Expand All @@ -133,7 +131,7 @@ describe('Trigger Conditions', () => {
})
).toBeVisible();

await user.click(
await userEvent.click(
screen.getByRole('option', {
name: evaluationPeriodOptions[0].label,
})
Expand Down Expand Up @@ -167,7 +165,7 @@ describe('Trigger Conditions', () => {
{ name: 'Open' }
);

user.click(pollingIntervalInput);
await userEvent.click(pollingIntervalInput);

expect(
await screen.findByRole('option', {
Expand All @@ -181,7 +179,7 @@ describe('Trigger Conditions', () => {
})
).toBeVisible();

await user.click(
await userEvent.click(
screen.getByRole('option', {
name: pollingIntervalOptions[0].label,
})
Expand All @@ -191,7 +189,7 @@ describe('Trigger Conditions', () => {
).toHaveAttribute('value', pollingIntervalOptions[0].label);
});

it('should be able to show the options that are greater than or equal to max scraping Interval', () => {
it('should be able to show the options that are greater than or equal to max scraping Interval', async () => {
renderWithThemeAndHookFormContext({
component: (
<TriggerConditions
Expand All @@ -216,7 +214,7 @@ describe('Trigger Conditions', () => {
{ name: 'Open' }
);

user.click(evaluationPeriodInput);
await userEvent.click(evaluationPeriodInput);

expect(
screen.queryByText(evaluationPeriodOptions[0].label)
Expand All @@ -227,7 +225,7 @@ describe('Trigger Conditions', () => {
'button',
{ name: 'Open' }
);
user.click(pollingIntervalInput);
await userEvent.click(pollingIntervalInput);
expect(
screen.queryByText(pollingIntervalOptions[0].label)
).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,72 +48,61 @@
scope: 'entity',
});
describe('EditAlertDefinition component', () => {
it(
'renders the components of the form',
async () => {
const { findByPlaceholderText, getByLabelText, getByText } =
renderWithTheme(
<EditAlertDefinition
alertDetails={alertDetails}
serviceType="linode"
/>
);
expect(getByText('1. General Information')).toBeVisible();
expect(getByLabelText('Name')).toBeVisible();
expect(getByLabelText('Description (optional)')).toBeVisible();
expect(getByLabelText('Severity')).toBeVisible();
expect(getByLabelText('Service')).toBeVisible();
expect(getByText('2. Entities')).toBeVisible();
expect(
await findByPlaceholderText('Search for a Region or Entity')
).toBeInTheDocument();
expect(await findByPlaceholderText('Select Regions')).toBeInTheDocument();
expect(getByText('3. Criteria')).toBeVisible();
expect(getByText('Metric Threshold')).toBeVisible();
expect(getByLabelText('Data Field')).toBeVisible();
expect(getByLabelText('Aggregation Type')).toBeVisible();
expect(getByLabelText('Operator')).toBeVisible();
expect(getByLabelText('Threshold')).toBeVisible();
expect(getByLabelText('Evaluation Period')).toBeVisible();
expect(getByLabelText('Polling Interval')).toBeVisible();
expect(getByText('4. Notification Channels')).toBeVisible();
},
{ timeout: 20000 }
);

it(
'should submit form data correctly',
async () => {
navigate({
to: '/alerts/definitions/edit/linode/1',
});
const mutateAsyncSpy = queryMocks.useEditAlertDefinition().mutateAsync;
const { getByPlaceholderText, getByText } = renderWithTheme(
it('renders the components of the form', { timeout: 20000 }, async () => {
const { findByPlaceholderText, getByLabelText, getByText } =
renderWithTheme(
<EditAlertDefinition alertDetails={alertDetails} serviceType="linode" />
);
const descriptionValue = 'Updated Description';
const nameValue = 'Updated Label';
const nameInput = getByPlaceholderText('Enter a Name');
const descriptionInput = getByPlaceholderText('Enter a Description');
await userEvent.clear(nameInput);
await userEvent.clear(descriptionInput);
await userEvent.type(nameInput, nameValue);
expect(getByText('1. General Information')).toBeVisible();
expect(getByLabelText('Name')).toBeVisible();
expect(getByLabelText('Description (optional)')).toBeVisible();
expect(getByLabelText('Severity')).toBeVisible();
expect(getByLabelText('Service')).toBeVisible();
expect(getByText('2. Entities')).toBeVisible();
expect(
await findByPlaceholderText('Search for a Region or Entity')

Check warning on line 63 in packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Don't wrap `findBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `findBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `findBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `findBy*` queries fail implicitly when element is not found","line":63,"column":13,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":63,"endColumn":34}
).toBeInTheDocument();
expect(await findByPlaceholderText('Select Regions')).toBeInTheDocument();

Check warning on line 65 in packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Don't wrap `findBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `findBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `findBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `findBy*` queries fail implicitly when element is not found","line":65,"column":18,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":65,"endColumn":39}
expect(getByText('3. Criteria')).toBeVisible();
expect(getByText('Metric Threshold')).toBeVisible();
expect(getByLabelText('Data Field')).toBeVisible();
expect(getByLabelText('Aggregation Type')).toBeVisible();
expect(getByLabelText('Operator')).toBeVisible();
expect(getByLabelText('Threshold')).toBeVisible();
expect(getByLabelText('Evaluation Period')).toBeVisible();
expect(getByLabelText('Polling Interval')).toBeVisible();
expect(getByText('4. Notification Channels')).toBeVisible();
});

await userEvent.type(descriptionInput, descriptionValue);
it('should submit form data correctly', { timeout: 10000 }, async () => {
navigate({
to: '/alerts/definitions/edit/linode/1',
});
const mutateAsyncSpy = queryMocks.useEditAlertDefinition().mutateAsync;
const { getByPlaceholderText, getByText } = renderWithTheme(
<EditAlertDefinition alertDetails={alertDetails} serviceType="linode" />
);
const descriptionValue = 'Updated Description';
const nameValue = 'Updated Label';
const nameInput = getByPlaceholderText('Enter a Name');
const descriptionInput = getByPlaceholderText('Enter a Description');
await userEvent.clear(nameInput);
await userEvent.clear(descriptionInput);
await userEvent.type(nameInput, nameValue);

await userEvent.click(getByText('Submit'));
await userEvent.type(descriptionInput, descriptionValue);

await waitFor(() => expect(mutateAsyncSpy).toHaveBeenCalledTimes(1));
await userEvent.click(getByText('Submit'));

expect(navigate).toHaveBeenLastCalledWith({
to: '/alerts/definitions',
});
await waitFor(() => {
expect(
getByText(UPDATE_ALERT_SUCCESS_MESSAGE) // validate whether snackbar is displayed properly
).toBeInTheDocument();
});
},
{ timeout: 10000 }
);
await waitFor(() => expect(mutateAsyncSpy).toHaveBeenCalledTimes(1));

expect(navigate).toHaveBeenLastCalledWith({
to: '/alerts/definitions',
});
await waitFor(() => {
expect(
getByText(UPDATE_ALERT_SUCCESS_MESSAGE) // validate whether snackbar is displayed properly

Check warning on line 104 in packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":104,"column":9,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":104,"endColumn":18}
).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { act, renderHook } from '@testing-library/react';
import { alertFactory, serviceTypesFactory } from 'src/factories';

import { useContextualAlertsState } from '../../Utils/utils';
import { transformDimensionValue } from '../CreateAlert/Criteria/DimensionFilterValue/utils';
import { alertDefinitionFormSchema } from '../CreateAlert/schemas';
import {
alertsFromEnabledServices,
arraysEqual,
convertAlertDefinitionValues,
convertAlertsToTypeSet,
convertSecondsToMinutes,
Expand All @@ -17,7 +17,6 @@ import {
getSchemaWithEntityIdValidation,
getServiceTypeLabel,
handleMultipleError,
transformDimensionValue,
} from './utils';

import type { AlertValidationSchemaProps } from './utils';
Expand Down Expand Up @@ -498,27 +497,3 @@ describe('transformDimensionValue', () => {
).toBe('Test_value');
});
});

describe('arraysEqual', () => {
it('should return true when both arrays are empty', () => {
expect(arraysEqual([], [])).toBe(true);
});
it('should return false when one array is empty and the other is not', () => {
expect(arraysEqual([], [1, 2, 3])).toBe(false);
});
it('should return true when arrays are undefined', () => {
expect(arraysEqual(undefined, undefined)).toBe(true);
});
it('should return false when one of the arrays is undefined', () => {
expect(arraysEqual(undefined, [1, 2, 3])).toBe(false);
});
it('should return true when arrays are equal', () => {
expect(arraysEqual([1, 2, 3], [1, 2, 3])).toBe(true);
});
it('should return false when arrays are not equal', () => {
expect(arraysEqual([1, 2, 3], [1, 2, 3, 4])).toBe(false);
});
it('should return true when arrays have same elements but in different order', () => {
expect(arraysEqual([1, 2, 3], [3, 2, 1])).toBe(true);
});
});
Loading
Loading