Skip to content

Commit

Permalink
Implement target swap/restore
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-vrijswijk committed Sep 26, 2024
1 parent d879eaa commit 6636e26
Show file tree
Hide file tree
Showing 31 changed files with 1,167 additions and 203 deletions.
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
node-version: 22.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpx playwright install chromium --with-deps
- run: pnpm build
- run: pnpm lint
- run: pnpm test
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "tsc && vite build",
"preview": "vite preview",
"deploy": "./deploy.sh",
"test": "vitest --ui",
"test": "vitest",
"coverage": "vitest run --coverage",
"lint": "npm run lint:prettier && npm run lint:eslint",
"lint:prettier": "prettier --check .",
Expand Down Expand Up @@ -49,28 +49,29 @@
"@swc-jotai/debug-label": "0.1.1",
"@swc-jotai/react-refresh": "0.2.0",
"@swc/core": "1.6.13",
"@testing-library/react": "16.0.1",
"@types/eslint__js": "8.42.3",
"@types/react": "18.3.8",
"@types/react-dom": "18.3.0",
"@types/uuid": "10.0.0",
"@vitejs/plugin-react-swc": "3.7.0",
"@vitest/browser": "2.1.1",
"@vitest/coverage-v8": "2.1.1",
"@vitest/ui": "2.1.1",
"eslint": "9.11.0",
"eslint-plugin-react": "7.36.1",
"globals": "15.9.0",
"happy-dom": "15.7.4",
"husky": "9.1.6",
"lint-staged": "15.2.10",
"lucuma-schemas": "0.100.0",
"playwright": "1.47.2",
"prettier": "3.3.3",
"sass": "1.79.3",
"typescript": "5.6.2",
"typescript-eslint": "8.6.0",
"vite": "5.4.7",
"vite-plugin-mkcert": "1.17.6",
"vitest": "2.1.1"
"vitest": "2.1.1",
"vitest-browser-react": "0.0.1"
},
"lint-staged": {
"*.{js,mjs,ts,tsx,jsx,css,md,json,yml}": "prettier --write"
Expand Down
421 changes: 384 additions & 37 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Home from './components/Layout/Home/Home';
import Layout from './components/Layout/Layout';
import Login from './components/Login/Login';
import Token from './components/Token/Token';
import { ToastProvider } from './Helpers/toast';

const router = createBrowserRouter([
{ path: '/', element: <Layout />, children: [{ index: true, element: <Home /> }] },
Expand All @@ -29,8 +30,10 @@ export function App({ environment }: { environment: Environment }) {

return (
<ApolloProvider client={client}>
<Modals />
<RouterProvider router={router} />
<ToastProvider>
<Modals />
<RouterProvider router={router} />
</ToastProvider>
</ApolloProvider>
);
}
25 changes: 25 additions & 0 deletions src/Helpers/toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Toast } from 'primereact/toast';
import { createContext, useContext, useEffect, useRef, useState } from 'react';

const ToastContext = createContext<Toast | null>(null);

export function useToast() {
return useContext(ToastContext);
}

export function ToastProvider({ children }: { children: React.ReactNode }) {
const ref = useRef<Toast>(null);
const [toast, setToast] = useState<Toast | null>(null);

// We have to wrap the ref in a state to force a re-render on initial mount, otherwise the context value will be null
useEffect(() => {
if (ref.current) setToast(ref.current);
}, [ref.current]);

return (
<>
<Toast ref={ref} />
<ToastContext.Provider value={toast}>{children}</ToastContext.Provider>
</>
);
}
10 changes: 5 additions & 5 deletions src/components/Contexts/Variables/Modals/SlewFlags.test.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { canEditAtom } from '@/components/atoms/auth';
import { slewVisibleAtom } from '@/components/atoms/slew';
import { renderWithContext } from '@gql/render';
import { screen, waitFor } from '@testing-library/react';
import { page } from '@vitest/browser/context';
import { SlewFlags } from './SlewFlags';

describe(SlewFlags.name, () => {
it('should render', async () => {
renderWithContext(<SlewFlags />, { initialValues: [[slewVisibleAtom, true]] });

expect(screen.getAllByRole<HTMLInputElement>('checkbox')).toHaveLength(16);
await waitFor(async () => (await screen.findByLabelText<HTMLInputElement>('Zero Chop Throw')).disabled);
await expect.element(page.getByText('Zero Chop Throw')).toBeEnabled();
expect(page.getByRole('checkbox').elements()).toHaveLength(16);
});

it('should disable when canEdit=false', () => {
it('should disable when canEdit=false', async () => {
renderWithContext(<SlewFlags />, {
initialValues: [
[slewVisibleAtom, true],
[canEditAtom, false],
],
});

expect(screen.getByLabelText<HTMLInputElement>('Zero Chop Throw').disabled).to.be.true;
await expect.element(page.getByLabelText('Zero Chop Throw')).toBeDisabled();
});
});
3 changes: 3 additions & 0 deletions src/components/Layout/Home/Home.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
width: 50px;
color: var(--primary-color-text);
top: 2px;
background: none;
color: inherit;
border: none;
cursor: pointer;
}

Expand Down
9 changes: 4 additions & 5 deletions src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useRef, useState } from 'react';
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { InputText } from 'primereact/inputtext';
import { Password } from 'primereact/password';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';
import './Login.scss';
import { useSignIn } from '../atoms/auth';
import { useToast } from '@/Helpers/toast';

interface LocationInterface {
from: {
Expand All @@ -20,7 +20,7 @@ export default function Login() {
const [password, setPassword] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const signin = useSignIn();
const toast = useRef<Toast>(null);
const toast = useToast();

const from = (location.state as LocationInterface)?.from?.pathname ?? '/';

Expand All @@ -36,7 +36,7 @@ export default function Login() {
// user experience.)
navigate(from, { replace: true });
} else {
toast.current?.show({
toast?.show({
severity: 'error',
summary: 'Login Error',
detail: 'Wrong credentials',
Expand All @@ -52,7 +52,6 @@ export default function Login() {

return (
<div className="login">
<Toast ref={toast} className="p-button-danger" />
<div className="box">
<div className="title">
<div className="text">Navigate</div>
Expand Down
50 changes: 26 additions & 24 deletions src/components/Panels/Guider/Alarms/Alarm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render, RenderResult, screen } from '@testing-library/react';
import { render, RenderResult } from 'vitest-browser-react';
import { Locator, page, userEvent } from '@vitest/browser/context';
import { ComponentProps } from 'react';
import { Alarm } from './Alarm';
import { MockedFunction } from 'vitest';
Expand All @@ -19,12 +20,12 @@ describe(Alarm.name, () => {
);
});

it('should render', () => {
expect(screen.getByLabelText('Counts').textContent).toEqual('1000');
expect(screen.getByLabelText('Subaperture').textContent).toEqual('OK');
it('should render', async () => {
await expect.element(page.getByLabelText('Counts')).toHaveTextContent('1000');
await expect.element(page.getByLabelText('Subaperture')).toHaveTextContent('OK');
});

it('should show NOK for no centroid', () => {
it('should show NOK for no centroid', async () => {
sut.rerender(
<Alarm
wfs="PWFS1"
Expand All @@ -34,28 +35,28 @@ describe(Alarm.name, () => {
onUpdateAlarm={onUpdateAlarm}
/>,
);
expect(screen.getByLabelText('Subaperture').textContent).toEqual('NOK');
await expect.element(page.getByLabelText('Subaperture')).toHaveTextContent('NOK');
});

it('should call onUpdateAlarm when enabled changes', () => {
it('should call onUpdateAlarm when enabled changes', async () => {
const checkbox = sut.getByRole('checkbox');

switchEnabled(checkbox);
await switchEnabled(checkbox);
expect(onUpdateAlarm).toHaveBeenCalledWith({ wfs: 'PWFS1', enabled: false });
});

it('should call onUpdateAlarm when limit changes', () => {
const inputNumber = screen.getByLabelText('Limit');
it('should call onUpdateAlarm when limit changes', async () => {
const inputNumber = page.getByLabelText('Limit');

setLimit(inputNumber, 100);
await setLimit(inputNumber, 100);
expect(onUpdateAlarm).toHaveBeenCalledWith({ wfs: 'PWFS1', limit: 100 });
});

it('should not set has-alarm if flux is above limit', () => {
expect(sut.container.querySelector('.has-alarm')).toBeNull();
it('should not set has-alarm if flux is above limit', async () => {
await expect.element(page.getByTestId('no-alarm')).toBeInTheDocument();
});

it('should set has-alarm if flux is below limit', () => {
it('should set has-alarm if flux is below limit', async () => {
sut.rerender(
<Alarm
wfs="PWFS1"
Expand All @@ -65,10 +66,10 @@ describe(Alarm.name, () => {
onUpdateAlarm={onUpdateAlarm}
/>,
);
expect(sut.container.querySelector('.has-alarm')).not.toBeNull();
await expect.element(page.getByTestId('has-alarm')).toBeInTheDocument();
});

it('should set has-alarm if centroidDetected is false', () => {
it('should set has-alarm if centroidDetected is false', async () => {
sut.rerender(
<Alarm
wfs="PWFS1"
Expand All @@ -78,10 +79,11 @@ describe(Alarm.name, () => {
onUpdateAlarm={onUpdateAlarm}
/>,
);
expect(sut.container.querySelector('.has-alarm')).not.toBeNull();

await expect.element(page.getByTestId('has-alarm')).toBeInTheDocument();
});

it('should not set has-alarm if disabled', () => {
it('should not set has-alarm if disabled', async () => {
sut.rerender(
<Alarm
wfs="PWFS1"
Expand All @@ -91,15 +93,15 @@ describe(Alarm.name, () => {
onUpdateAlarm={onUpdateAlarm}
/>,
);
expect(sut.container.querySelector('.has-alarm')).toBeNull();
await expect.element(page.getByTestId('no-alarm')).toBeInTheDocument();
});
});

function setLimit(el: HTMLElement, value: number) {
fireEvent.change(el, { target: { value } });
fireEvent.blur(el);
async function setLimit(el: Locator, value: number) {
await userEvent.fill(el, value.toString());
await userEvent.tab();
}

function switchEnabled(el: HTMLElement) {
fireEvent.click(el);
async function switchEnabled(el: Locator) {
await userEvent.click(el);
}
5 changes: 4 additions & 1 deletion src/components/Panels/Guider/Alarms/Alarm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export function Alarm({
const hasAlarm = evaluateAlarm(alarm, guideQuality);

return (
<div className={clsx('alarm', hasAlarm && 'has-alarm animate-error-bg')}>
<div
data-testid={hasAlarm ? 'has-alarm' : 'no-alarm'}
className={clsx('alarm', hasAlarm && 'has-alarm animate-error-bg')}
>
<div className="title-bar">
<Title title={wfs} />
</div>
Expand Down
21 changes: 9 additions & 12 deletions src/components/Panels/Guider/Alarms/Alarms.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,31 @@ import { MockedResponse } from '@apollo/client/testing';
import { GET_GUIDE_ALARMS, UPDATE_GUIDE_ALARM } from '@gql/configs/GuideAlarm';
import { renderWithContext } from '@gql/render';
import { GUIDE_QUALITY_SUBSCRIPTION } from '@gql/server/GuideQuality';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { Alarms, evaluateAlarm } from './Alarms';
import { guideAlarmAtom } from '@/components/atoms/alarm';
import { page, userEvent } from '@vitest/browser/context';

describe(Alarms.name, () => {
let store: ReturnType<typeof renderWithContext>['store'];
beforeEach(async () => {
store = renderWithContext(<Alarms />, { mocks }).store;

// Wait for the alarms to be loaded
await waitFor(async () => !(await screen.findAllByLabelText<HTMLInputElement>('Limit'))[0].disabled);
await expect.element(page.getByText('PWFS1')).toBeEnabled();
});

it('should render 3 alarms', () => {
expect(screen.queryByText('PWFS1')).not.toBeNull();
expect(screen.queryByText('PWFS2')).not.toBeNull();
expect(screen.queryByText('OIWFS')).not.toBeNull();
it('should render 3 alarms', async () => {
await expect.element(page.getByText('PWFS1')).toBeInTheDocument();
await expect.element(page.getByText('PWFS2')).toBeInTheDocument();
await expect.element(page.getByText('OIWFS')).toBeInTheDocument();
});

it('calls updateAlarm when limit is changed', async () => {
const limitInput = screen.getAllByLabelText('Limit')[0];
const limitInput = page.getByLabelText('Limit').elements()[0];

fireEvent.change(limitInput, { target: { value: '900' } });
fireEvent.blur(limitInput);
await userEvent.fill(limitInput, '900');

await waitFor(async () =>
expect((await screen.findAllByLabelText<HTMLInputElement>('Limit'))[0].value).equals('900'),
);
await expect.element(page.getByLabelText('Limit').elements()[0]).toHaveValue('900');
expect(store.get(guideAlarmAtom)).true;
});
});
Expand Down
14 changes: 6 additions & 8 deletions src/components/Panels/Telescope/Targets/GuiderTargets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Dropdown } from 'primereact/dropdown';
import { ProgressBar } from 'primereact/progressbar';
import { Title } from '@Shared/Title/Title';
import { TargetList } from './TargetList';
import { Button } from 'primereact/button';
import { UpdateGuideTargets } from './UpdateGuideTargets';

import { useLoadingGuideTargetValue } from '@/components/atoms/guideTarget';
import { useCanEdit } from '@/components/atoms/auth';
import { useConfiguration } from '@gql/configs/Configuration';
import { useTargets } from '@gql/configs/Target';
import { TargetSwapButton } from './TargetSwapButton';
import { Target } from '@gql/configs/gen/graphql';

function GuiderFooter({ disabled }: { disabled: boolean }) {
return (
Expand All @@ -24,10 +25,12 @@ function GuiderFooter({ disabled }: { disabled: boolean }) {

export function GuiderTargets() {
const canEdit = useCanEdit();
const { oiTargets, p1Targets, p2Targets } = useTargets().data;
const { oiTargets, p1Targets, p2Targets, allTargets } = useTargets().data;
const configuration = useConfiguration().data?.configuration;
const loadingGuideTarget = useLoadingGuideTargetValue();

const selectedTarget: Target | undefined = allTargets.find((t) => t.pk === configuration?.selectedTarget);

const displayProbes: JSX.Element[] = [];
if (oiTargets.length > 0) {
const oiSelected = oiTargets.find((t) => t.pk === configuration?.selectedOiTarget);
Expand Down Expand Up @@ -83,12 +86,7 @@ export function GuiderTargets() {
displayProbes
)}
</div>
<Button
disabled={!canEdit} // check is a valid target || !Boolean(selectedGuideTarget?.id)
className="footer w-100"
label="Point to guide target"
onClick={() => console.log('Point to guide target')}
/>
<TargetSwapButton selectedTarget={selectedTarget} />
</div>
);
}
Loading

0 comments on commit 6636e26

Please sign in to comment.