Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e6a859d
feat: new check editor
w1kman Oct 6, 2025
33be75d
feat: new check editor
w1kman Oct 6, 2025
6451c53
feat: new check editor
w1kman Oct 6, 2025
6b3c1cb
feat: new check editor
w1kman Oct 6, 2025
7bf7d2d
feat: new check editor
w1kman Oct 6, 2025
34b340a
feat: new check editor
w1kman Oct 6, 2025
eaf6737
feat: new check editor
w1kman Oct 6, 2025
33c4ac4
feat: new check editor
w1kman Oct 6, 2025
56daafe
feat: new check editor
w1kman Oct 6, 2025
347006d
feat: new check editor
w1kman Oct 6, 2025
7e12760
feat: new check editor
w1kman Oct 7, 2025
272b0c3
feat: new check editor
w1kman Oct 7, 2025
2f546bc
feat: new check editor
w1kman Oct 7, 2025
7e40a88
feat: new check editor
w1kman Oct 8, 2025
3778599
feat: new check editor
w1kman Oct 8, 2025
ac3cf1b
feat: new check editor
w1kman Oct 8, 2025
13c0345
feat: new check editor
w1kman Oct 8, 2025
414d046
feat: new check editor
w1kman Oct 9, 2025
8c69ce5
feat: new check editor
w1kman Oct 9, 2025
e8c2936
feat: new check editor
w1kman Oct 9, 2025
ea3f2b9
feat: new check editor
w1kman Oct 9, 2025
2c8481b
feat: new check editor
w1kman Oct 9, 2025
54a2a20
feat: new check editor
w1kman Oct 9, 2025
03d316c
feat: new check editor
w1kman Oct 9, 2025
0855e38
feat: new check editor
w1kman Oct 9, 2025
f6428b1
feat: new check editor
w1kman Oct 10, 2025
cc77911
feat: new check editor
w1kman Oct 10, 2025
e37327f
feat: new check editor
w1kman Oct 10, 2025
f65f5f9
feat: new check editor
w1kman Oct 12, 2025
ec6d20d
feat: new check editor
w1kman Oct 12, 2025
7c49aa9
feat: new check editor
w1kman Oct 13, 2025
823bde9
feat: new check editor
w1kman Oct 14, 2025
45601fc
feat: new check editor
w1kman Oct 14, 2025
5aab4c2
feat: new check editor
w1kman Oct 14, 2025
c9d75de
feat: new check editor
w1kman Oct 14, 2025
ba017a2
feat: new check editor
w1kman Oct 14, 2025
638453f
feat: new check editor
w1kman Oct 14, 2025
7a1a95e
feat: new check editor
w1kman Oct 15, 2025
e858ee1
feat: new check editor
w1kman Oct 15, 2025
b5d673d
Merge branch 'main' into w1kman/new-check-editor
w1kman Oct 15, 2025
7d6ddc4
feat: new check editor
w1kman Oct 15, 2025
97266ec
feat: new check editor
w1kman Oct 15, 2025
f75ea9a
Merge branch 'main' into w1kman/new-check-editor
w1kman Oct 15, 2025
4964e88
feat: new check editor
w1kman Oct 16, 2025
418ce12
feat: new check editor
w1kman Oct 22, 2025
ddaf5e5
feat: new check editor
w1kman Oct 22, 2025
04cf6f1
feat: new check editor
w1kman Oct 22, 2025
88f227c
feat: new check editor
w1kman Oct 22, 2025
1e3d297
feat: new check editor
w1kman Oct 23, 2025
6537654
feat: new check editor
w1kman Oct 23, 2025
e8579a9
chore: add tests for `NewCheckV2.tsx`
w1kman Oct 23, 2025
598e877
chore: add tests for `NewCheckV2.tsx`
w1kman Oct 24, 2025
3d8f64b
chore: add tests for `NewCheckV2.tsx`
w1kman Oct 24, 2025
34fdd44
chore: add tests for `NewCheckV2.tsx`
w1kman Oct 24, 2025
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
1 change: 1 addition & 0 deletions src/components/CheckEditor/CheckProbes/ProbesFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const ProbesFilter = ({
showEmptyState={showEmptyState}
emptyText="There are no probes matching your criteria."
placeholder="Find a probe by city, country, region or provider"
data-form-name="probes"
/>
</>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/CheckEditor/FormComponents/Frequency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const Frequency = ({ checkType, disabled }: ProbeOptionsProps) => {
className={styles.field}
id={FREQUENCY_INPUT_ID}
data-testid={DataTestIds.FREQUENCY_COMPONENT}
data-form-name="frequency" // this is used to assist form when trying to scroll/focus error field (new check editor)
>
<Stack direction="column" gap={1.5}>
<Stack direction="column" gap={0.5}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const FrequencyCustom = ({ value, onChange, min, max, disabled }: Frequen
onKeyDown={handleKeyDown}
id={FREQUENCY_MINUTES_INPUT_ID}
disabled={disabled}
data-form-name="frequency" // used for form error focusing (new check editor)
/>
</Field>
<Field label="Seconds" className={styles.field}>
Expand Down
5 changes: 3 additions & 2 deletions src/components/CheckEditor/ProbeOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { CheckProbes } from './CheckProbes/CheckProbes';

interface ProbeOptionsProps {
checkType: CheckType;
onlyProbes?: boolean; // TODO: Remove when CheckEditor v1 is removed
}

export const ProbeOptions = ({ checkType }: ProbeOptionsProps) => {
export const ProbeOptions = ({ checkType, onlyProbes }: ProbeOptionsProps) => {
const { data: probes = [] } = useProbesWithMetadata();
const {
control,
Expand Down Expand Up @@ -41,7 +42,7 @@ export const ProbeOptions = ({ checkType }: ProbeOptionsProps) => {
error={errors.probes?.message}
onChange={handleChange}
/>
<Frequency checkType={checkType} disabled={disabled} />
{!onlyProbes && <Frequency checkType={checkType} disabled={disabled} />}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/CheckForm/CheckForm.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
const checkTypeGroup = useFormCheckTypeGroup(check);
const schema = useCheckFormSchema(check);
const options = useCheckTypeOptions();
const isOverLimit = useIsOverlimit(!isNew, checkType);

Check failure on line 75 in src/components/CheckForm/CheckForm.hooks.ts

View workflow job for this annotation

GitHub Actions / Lint / set-up

`useIsOverlimit` is deprecated. use `hooks/useIsOverlimit` instead
const permission = useFormPermissions();
const defaultFormValues = useCheckFormDefaultValues(check);
const isExistingCheck = getIsExistingCheck(check);
Expand Down Expand Up @@ -246,6 +246,7 @@
}, [canReadLogs, canWriteChecks]);
}

/** @deprecated use `hooks/useIsOverlimit` instead */
export function useIsOverlimit(isExistingCheck: boolean, checkType: CheckType) {
const { isOverBrowserLimit, isOverHgExecutionLimit, isOverCheckLimit, isOverScriptedLimit, isReady } = useLimits();
// It should always be possible to edit existing checks
Expand Down
263 changes: 263 additions & 0 deletions src/components/Checkster/Checkster.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { render } from 'test/render';

import { Check, CheckType } from 'types';

import { ChecksterProvider, useChecksterContext } from './contexts/ChecksterContext';
import { Checkster } from './Checkster';
import { DEFAULT_CHECK_CONFIG } from './constants';

// Mock child components that have complex dependencies
jest.mock('./components/form/FormRoot', () => ({
FormRoot: jest.fn(({ onSave }) => (
<div data-testid="form-root">
FormRoot
<button onClick={() => onSave(DEFAULT_CHECK_CONFIG, {})}>Save</button>
</div>
)),
}));

jest.mock('./components/FormSectionNavigation/FormSectionNavigation', () => ({
FormSectionNavigation: () => <div data-testid="form-section-navigation">FormSectionNavigation</div>,
}));

jest.mock('./feature/FeatureTabs', () => ({
FeatureTabs: () => <div data-testid="feature-tabs">FeatureTabs</div>,
}));

jest.mock('./feature/FeatureContent', () => ({
FeatureContent: () => <div data-testid="feature-content">FeatureContent</div>,
}));

jest.mock('../ConfirmLeavingPage', () => ({
ConfirmLeavingPage: ({ enabled }: { enabled: boolean }) =>
enabled ? <div data-testid="confirm-leaving-page">Confirm Leaving</div> : null,
}));

const mockOnSave = jest.fn();

const defaultProps = {
onSave: mockOnSave,
};

// Test component to verify context usage
function TestContextConsumer() {
try {
const context = useChecksterContext();
return <div data-testid="context-consumer">Context Available: {context.formId}</div>;
} catch (error) {
return <div data-testid="context-consumer">Context Not Available</div>;
}
}

describe('Checkster.tsx', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('Conditional context', () => {
it('should render conditional context when not in ChecksterContext', async () => {
render(<Checkster {...defaultProps} />);

// Verify that the component renders without throwing context errors
await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
});
});

it('should render without conditional context when already in ChecksterContext', async () => {
render(
<ChecksterProvider>
<Checkster {...defaultProps} />
</ChecksterProvider>
);

// Verify that the component renders when already wrapped in context
await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
});
});

it('should demonstrate InternalConditionalProvider behavior with nested context', async () => {
// Test that InternalConditionalProvider doesn't create duplicate providers
let renderCount = 0;

const CountingProvider = ({ children }: { children: React.ReactNode }) => {
renderCount++;
return <ChecksterProvider>{children}</ChecksterProvider>;
};

render(
<CountingProvider>
<TestContextConsumer />
</CountingProvider>
);

await waitFor(() => {
expect(screen.getByTestId('context-consumer')).toBeInTheDocument();
});

// Should only render once since we're providing the context externally
expect(renderCount).toBe(1);
});
});

describe('ChecksterInternal component behavior', () => {
it('should render loading state when isLoading is true', async () => {
// Mock the context to return loading state
const MockChecksterProvider = ({ children }: { children: React.ReactNode }) => (
<ChecksterProvider>{children}</ChecksterProvider>
);

render(
<MockChecksterProvider>
<Checkster {...defaultProps} />
</MockChecksterProvider>
);

// Check that AppContainer is rendered (which handles loading state)
await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
});
});

it('should render form components when loaded', async () => {
render(<Checkster {...defaultProps} />);

await waitFor(() => {
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('feature-tabs')).toBeInTheDocument();
expect(screen.getByTestId('feature-content')).toBeInTheDocument();
});
});

it('should not render ChooseCheckTypeModal by default', async () => {
render(<Checkster {...defaultProps} />);

await waitFor(() => {
expect(screen.queryByTestId('choose-check-type-modal')).not.toBeInTheDocument();
});
});
});

describe('Props handling and callbacks', () => {
it('should handle onSave callback', async () => {
const mockResolvedFunction = jest.fn();
mockOnSave.mockResolvedValue(mockResolvedFunction);

const { user } = render(<Checkster {...defaultProps} />);

await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
});

const saveButton = screen.getByRole('button', { name: 'Save' });
await user.click(saveButton);

expect(mockOnSave).toHaveBeenCalledWith(DEFAULT_CHECK_CONFIG, {});
});

it('should accept and use check prop', async () => {
const customCheck: Check = {
...DEFAULT_CHECK_CONFIG,
job: 'custom-check-job',
target: 'https://example.com',
};

render(<Checkster {...defaultProps} check={customCheck} />);

await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
});
});

it('should handle check type via props', async () => {
render(<Checkster {...defaultProps} checkType={CheckType.HTTP} />);

await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
});
});
});

describe('Form integration', () => {
it('should show ConfirmLeavingPage when form is dirty', async () => {
// This test would need more setup to actually make the form dirty
// For now, we'll just verify the component structure
render(<Checkster {...defaultProps} />);

await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
});

// The ConfirmLeavingPage component should be present but not visible (enabled=false by default)
expect(screen.queryByTestId('confirm-leaving-page')).not.toBeInTheDocument();
});

it('should render proper layout structure', async () => {
render(<Checkster {...defaultProps} />);

await waitFor(() => {
// Verify the main layout components are present
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('feature-tabs')).toBeInTheDocument();
expect(screen.getByTestId('feature-content')).toBeInTheDocument();
});
});
});

describe('Error handling', () => {
it('should render successfully even when onSave might fail', async () => {
// Test that the component renders and functions properly regardless of onSave implementation
const mockFailingOnSave = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
// This simulates an async operation that could fail
setTimeout(() => reject(new Error('Simulated save failure')), 0);
})
);

render(<Checkster {...defaultProps} onSave={mockFailingOnSave} />);

await waitFor(() => {
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
expect(screen.getByTestId('feature-tabs')).toBeInTheDocument();
expect(screen.getByTestId('feature-content')).toBeInTheDocument();
});

// Component should render successfully regardless of potential onSave failures
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
});
});

describe('Context integration', () => {
it('should provide context that can be accessed by ChecksterInternal', async () => {
render(<Checkster {...defaultProps} />);

await waitFor(() => {
// Verify components that depend on context are rendered successfully
expect(screen.getByTestId('form-root')).toBeInTheDocument();
expect(screen.getByTestId('form-section-navigation')).toBeInTheDocument();
});
});

it('should properly integrate with external ChecksterProvider', async () => {
render(
<ChecksterProvider>
<TestContextConsumer />
</ChecksterProvider>
);

await waitFor(() => {
expect(screen.getByTestId('context-consumer')).toBeInTheDocument();
expect(screen.getByTestId('context-consumer')).toHaveTextContent('Context Available:');
});
});
});
});
53 changes: 53 additions & 0 deletions src/components/Checkster/Checkster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';

import { Check, CheckFormValues } from '../../types';

import { ConfirmLeavingPage } from '../ConfirmLeavingPage';
import { FormRoot } from './components/form/FormRoot';
import { FormSectionNavigation } from './components/FormSectionNavigation/FormSectionNavigation';
import { AppContainer } from './components/ui/AppContainer';
import { PrimaryLayoutSection } from './components/ui/PrimaryLayoutSection';
import { SecondaryLayoutSection } from './components/ui/SecondaryLayoutSection';
import { ChecksterProviderProps, InternalConditionalProvider, useChecksterContext } from './contexts/ChecksterContext';
import { FeatureTabsContextProvider } from './contexts/FeatureTabsContext';
import { FeatureContent } from './feature/FeatureContent';
import { FeatureTabs } from './feature/FeatureTabs';

type ChecksterProps = ChecksterProviderProps & {
// Resolve with a function if a callback should be made after the fact
// that the check is saved (and the form knows everything when OK)
// Example: when we want to navigate from the form after a successful save
onSave(check: Check, formValues: CheckFormValues): Promise<Function | void>;
};

export function Checkster({ onSave, ...props }: ChecksterProps) {
return (
<InternalConditionalProvider {...props}>
<ChecksterInternal onSave={onSave} />
</InternalConditionalProvider>
);
}

function ChecksterInternal({ onSave }: ChecksterProps) {
const { isLoading, error } = useChecksterContext();
const {
formState: { isDirty },
} = useFormContext();

return (
<>
<AppContainer isLoading={isLoading} error={error}>
<PrimaryLayoutSection headerContent={<FormSectionNavigation />}>
<FormRoot onSave={onSave} />
</PrimaryLayoutSection>
<FeatureTabsContextProvider>
<SecondaryLayoutSection headerContent={<FeatureTabs />}>
<FeatureContent />
</SecondaryLayoutSection>
</FeatureTabsContextProvider>
</AppContainer>
<ConfirmLeavingPage enabled={isDirty} />
</>
);
}
25 changes: 25 additions & 0 deletions src/components/Checkster/__testHelpers__/formHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { screen, within } from '@testing-library/react';
import { UserEvent } from '@testing-library/user-event';

import { FormSectionName } from '../types';

import { CHECKSTER_TEST_ID, DEFAULT_FORM_SECTION_ORDER } from '../constants';

export async function gotoSection(user: UserEvent, section: FormSectionName | number) {
const tabList = screen.getByTestId(CHECKSTER_TEST_ID.navigation.root);
const buttonIndex =
typeof section === 'number'
? section
: DEFAULT_FORM_SECTION_ORDER.findIndex((formSectionName) => formSectionName === section);

const tabs = within(tabList).getAllByRole('tab');
const tab = tabs[buttonIndex];

await user.click(tab);
}

export async function submitForm(user: UserEvent) {
const submitButton = screen.getByTestId(CHECKSTER_TEST_ID.form.submitButton);
// expect(submitButton).toBeInTheDocument();
await user.click(submitButton);
}
Loading
Loading