Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,9 @@ iaso_3_9_18
/temp
iaso-enketo/

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
6 changes: 3 additions & 3 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ case "$1" in
./scripts/wait_for_dbs.sh
# Run python tests and pass on any args to e.g. run individual tests
./manage.py test --exclude-tag selenium "${@:2}"
npm run mocha
npm run test
;;
"test_lint" )
export TESTING=true
Expand All @@ -57,8 +57,8 @@ case "$1" in
"test_js" )
npm run test
;;
"mocha" )
npm run mocha
"vitest" )
npm run test
;;
"gen_docs" )
./scripts/gen_docs.sh
Expand Down
26 changes: 25 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import vitest from '@vitest/eslint-plugin';
import { defineConfig, globalIgnores } from 'eslint/config';
import formatjs from 'eslint-plugin-formatjs';
import importPlugin from 'eslint-plugin-import';
Expand Down Expand Up @@ -246,7 +247,6 @@ export default defineConfig([
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
},
},

rules: {
'import/extensions': [
'error',
Expand Down Expand Up @@ -361,5 +361,29 @@ export default defineConfig([
'react/require-default-props': 'off',
'valid-typeof': 'warn',
},
overrides: [
{
files: ['**/*.test.tsx', '**/*.test.ts'],
plugins: {
vitest,
},
rules: {
...vitest.configs.recommended.rules,
},
globals: {
...vitest.environments.env.globals,
...globals.jest,
},
},
{
files: ['playwright.config.ts', '**/playwright/**/*.test.ts'],
env: {
node: true,
},
rules: {
'no-process-env': 'off',
},
},
],
},
]);
19 changes: 19 additions & 0 deletions hat/assets/js/__tests__/integration/Duplicates.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { useGetDuplicates } from '../../apps/Iaso/domains/entities/duplicates/hooks/api/useGetDuplicates';

// just an example on how to integrate testing without backend => using mock

vi.mock('../../apps/Iaso/domains/entities/duplicates/hooks/api/useGetDuplicates', () => ({
useGetDuplicates: vi.fn(),
}));

describe('Duplicates page integration testing', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('Outputs no results when there is none', () => {
useGetDuplicates.mockResolvedValue([]);
// there we check by submitting that the table returns "no results", mock avoids calling the backend.
});
});
27 changes: 27 additions & 0 deletions hat/assets/js/__tests__/playwright/e2e/example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test, expect } from '@playwright/test';

// e2e testing needs the backend.

test('an example of e2e : create an user and login', async ({ page }) => {
await page.goto(`/login/?next=${encodeURIComponent('/dashboard/home/')}`)
await page.fill('input[name="username"]', process.env?.LOGIN_USERNAME ?? '')
await page.fill('input[name="password"]', process.env?.LOGIN_PASSWORD ?? '')
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard/setupAccount');
await expect(page).toHaveTitle(/Welcome/);

// we fill in the form with a too simple password
const username = `iaso${Math.random()}`
await page.fill("input#input-text-account_name", "An account name")
await page.fill("input#input-text-user_username", username)
await page.fill("input#input-text-user_first_name", "Iaso first name")
await page.fill("input#input-text-user_last_name", "Iaso last name")
await page.fill("input#input-text-user_email", "test@test.com")

await page.fill("input#input-text-password", "1234")

await page.click('button[data-test="confirm-button"]');
expect(page.getByRole('heading', {name: 'Account and profile created successfully, please logout and login again with the new profile.', level: 6})).toBeDefined()

// we could go further on and check that if an user already exists , creating the same one would display an error on the UI
});
13 changes: 13 additions & 0 deletions hat/assets/js/__tests__/playwright/setup/setup.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import dotenv from 'dotenv';

export function loadEnv(project: 'smoke' | 'e2e', required: Array<any>): void {
dotenv.config({ path: `.env.${project}` });

for (const key of required) {
if (!process.env[key]) {
throw new Error(
`Missing ${key} in .env.${project}`
);
}
}
}
17 changes: 17 additions & 0 deletions hat/assets/js/__tests__/playwright/smoke/example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { test, expect } from '@playwright/test';

test('login page appears', async ({ page }) => {
await page.goto('');

await expect(page).toHaveURL(`/login/?next=${encodeURIComponent('/dashboard/home/')}`)
await expect(page).toHaveTitle("IASO")
});

test('login works', async({page}) => {
await page.goto(`/login/?next=${encodeURIComponent('/dashboard/home/')}`)
await page.fill('input[name="username"]', process.env?.LOGIN_USERNAME ?? '')
await page.fill('input[name="password"]', process.env?.LOGIN_PASSWORD ?? '')
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard/setupAccount');
await expect(page).toHaveTitle(/Welcome/);
})
16 changes: 16 additions & 0 deletions hat/assets/js/apps/Iaso/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Accordion } from './Accordion';

describe('Accordion', () => {
it('renders children', () => {
render(
<Accordion>
<div>Content</div>
</Accordion>,
);

expect(screen.getByText('Content')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { textPlaceholder } from 'bluesquare-components';
import moment from 'moment';
import { textPlaceholder } from 'bluesquare-components';
import { LANGUAGE_CONFIGS } from 'IasoModules/language/configs';
import { apiDateFormats, getLocaleDateFormat } from '../../utils/dates.ts';
import { apiDateFormats, getLocaleDateFormat } from 'Iaso/utils/dates';
import {
DateCell,
DateTimeCell,
DateTimeCellRfc,
convertValueIfDate,
} from './DateTimeCell.tsx';
} from 'Iaso/components/Cells/DateTimeCell';

const locales = Object.keys(LANGUAGE_CONFIGS);
const setLocale = code => {
moment.locale(code);
moment.updateLocale(code, {
longDateFormat:
LANGUAGE_CONFIGS[code]?.dateFormats ||
LANGUAGE_CONFIGS.en?.dateFormats ||
{},
week: {
dow: 1,
},
});
};
import {setLocale} from '../../../../tests/helpers';

describe('DateTimeCell', () => {
beforeEach(() => {
Expand Down Expand Up @@ -74,7 +63,7 @@ describe('DateCell', () => {
expect(DateCell(cellInfo)).to.equal(textPlaceholder);
});
it('should return the formatted date if value is a timestamp', () => {
const cellInfo = { value: 1627545600000 }; // timestamp for 2021-07-29
const cellInfo = { value: "1627545600000" }; // timestamp for 2021-07-29
const expected = moment(cellInfo.value).format(
getLocaleDateFormat('L'),
);
Expand Down
115 changes: 115 additions & 0 deletions hat/assets/js/apps/Iaso/components/forms/ErrorsPopper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ErrorsPopper } from './ErrorsPopper';
import { act } from '@testing-library/react';

// we mock Popper to avoid heavy computation and layout simulation
vi.mock('@mui/material', async () => {
const actual = await vi.importActual<typeof import('@mui/material')>(
'@mui/material',
);

return {
...actual,
Popper: ({ open, children }: any) =>
open ? <div data-testid="mock-popper">{children}</div> : null,
};
});


describe('ErrorsPopper', () => {
it('returns null when there are no errors', () => {
const { container } = render(
<ErrorsPopper errors={[]} errorCountMessage="0 errors" />,
);

expect(container.firstChild).toBeNull();
});

it('renders error count message when errors exist', () => {
render(
<ErrorsPopper
errors={['first error']}
errorCountMessage="1 error"
/>,
);

expect(screen.getByText('1 error')).toBeInTheDocument();
});

it('does not show errors initially', () => {
render(
<ErrorsPopper
errors={['first error']}
errorCountMessage="1 error"
/>,
);

// Popper closed by default
expect(screen.queryByText('First error')).not.toBeInTheDocument();
});

it('shows errors when icon is clicked', async () => {
const user = userEvent.setup();

render(
<ErrorsPopper
errors={['first error', 'second error']}
errorCountMessage="2 errors"
/>,
);

const button = screen.getByRole('button');

await act(async () => {
await user.click(button);
});


expect(screen.getByText('First error')).toBeInTheDocument();
expect(screen.getByText('Second error')).toBeInTheDocument();
});

it('toggles popper visibility when clicked twice', async () => {
const user = userEvent.setup();

render(
<ErrorsPopper
errors={['first error']}
errorCountMessage="1 error"
/>,
);

const button = screen.getByRole('button');

// Open
await act(async () => {
await user.click(button);
});
expect(screen.getByText('First error')).toBeInTheDocument();

// Close
await act(async () => {
await user.click(button);
});
expect(screen.queryByText('First error')).not.toBeInTheDocument();
});

it('capitalizes the first letter of each error', async () => {
const user = userEvent.setup();

render(
<ErrorsPopper
errors={['lowercase error']}
errorCountMessage="1 error"
/>,
);
await act(async () => {
await user.click(screen.getByRole('button'));
})


expect(screen.getByText('Lowercase error')).toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions hat/assets/js/apps/Iaso/domains/devices/config.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { Column, textPlaceholder, useSafeIntl } from 'bluesquare-components';
import MESSAGES from './messages';
import { DateTimeCell } from '../../components/Cells/DateTimeCell';
import { YesNoCell } from '../../components/Cells/YesNoCell';
import { DateTimeCell } from 'Iaso/components/Cells/DateTimeCell';
import { YesNoCell } from 'Iaso/components/Cells/YesNoCell';

export const useDevicesTableColumns = (): Column[] => {
const { formatMessage } = useSafeIntl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import { IconButton as IconButtonComponent } from 'bluesquare-components';
import omit from 'lodash/omit';
import { FormattedMessage } from 'react-intl';
import ConfirmCancelDialogComponent from '../../../components/dialogs/ConfirmCancelDialogComponent';
import { baseUrls } from '../../../constants/urls.ts';
import { useFormState } from '../../../hooks/form';
import { baseUrls } from 'Iaso/constants/urls';
import { useFormState } from 'Iaso/hooks/form';
import {
hasFeatureFlag,
SHOW_LINK_INSTANCE_REFERENCE,
} from '../../../utils/featureFlags';
import * as Permission from '../../../utils/permissions.ts';
} from 'Iaso/utils/featureFlags';
import * as Permission from '../../../utils/permissions';
import {
useCheckUserHasWritePermissionOnOrgunit,
useCurrentUser,
} from '../../../utils/usersUtils.ts';
} from 'Iaso/utils/usersUtils';
import { useSaveOrgUnit } from '../../orgUnits/hooks';
import { userHasPermission } from '../../users/utils';
import { REFERENCE_FLAG_CODE, REFERENCE_UNFLAG_CODE } from '../constants';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import moment from 'moment';
import { longDateFormats } from '../../../utils/dates';
import { longDateFormats } from 'Iaso/utils/dates';
import { formatValue } from '.';

const setLocale = code => {
const setLocale = (code: string) => {
moment.locale(code);
moment.updateLocale(code, {
longDateFormat: longDateFormats[code],
Expand All @@ -11,8 +11,9 @@ const setLocale = code => {
},
});
};

describe('formatValue', () => {
before(() => {
beforeAll(() => {
setLocale('en');
});
it('should leave number as is', () => {
Expand Down
Loading
Loading