Skip to content

Commit

Permalink
chore(itests): add more integration tests and fix CI integration (cry…
Browse files Browse the repository at this point in the history
…ostatio#1062)

Signed-off-by: Max Cao <[email protected]>
Co-authored-by: Thuan Vo <[email protected]>
  • Loading branch information
maxcao13 and Thuan Vo authored Aug 24, 2023
1 parent c185969 commit 0b61f98
Show file tree
Hide file tree
Showing 8 changed files with 499 additions and 116 deletions.
106 changes: 106 additions & 0 deletions LOCALIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
## Localization with react-i18next

Cryostat-web uses [i18next](https://www.i18next.com/) as an internationalization-framework. The react-i18next package is used to integrate i18next with React. To add new user strings to the project, they must be handled in the manner below.

### Adding a new translation

The current list of language locales supported in Cryostat can be found in `src/i18n/config.ts`. The translations themselves can be found in `locales/{LOCALE_CODE}`

To add a new language, add a new entry to the `i18nResources` object in `src/i18n.ts`. The key should be the language locale, and the value should be the translation object containing the corresponding namespace json files in `locales`.

To add a new localization key for a user-facing string in `cryostat-web`, use the `t` function from `react-i18next`:

```tsx
import { useTranslation } from 'react-i18next';
...
export const SomeFC = (props) => {
const { t } = useTranslation();

return (
<div>
{t('somekey')}
</div>
);
}
```
After saving the file, and running `yarn localize`, this will generate a new key in the `en` locale namespace json file in `/locales/en/common.json` *(having multiple locales will add a key to each locale json file!)*:
```bash
$ yarn localize # uses i18next-parser to generate based on files in src/
```
`locales/en/common.json`
```json
{
...
"somekey": "cryostat_tmp",
...
}
```

The value of the key will be the string `cryostat_tmp` by default (we set this in `i18next-parser.config.js`). This is a placeholder value that should be replaced with the actual translation by going to the corresponding locale json file and manually replacing the value with the translation.

`locales/en/common.json`
```json
{
...
"somekey": "This is a translation",
...
}
```



The React i18next API docs can be found [here](https://react.i18next.com/latest/using-with-hooks).

### Cryostat locale namespaces

Currently the two namespaces are `common` and `public`.

If you want to add a new key to a specific namespace, you can specify the namespace as the first argument to the `t` function:

```tsx
<div>
{t('SOME_COMMON_KEY', { ns: 'common' })}
</div>
```

In `cryostat-web`, we use `common` for common user-facing strings that you may see all the time: e.g. `Home`, `Help`, `Cancel`, etc.


```tsx
<div>
{t('Cancel')}
</div>
```

`locales/en/common.json`
```json
{
"CANCEL": "Cancel",
}
```
These keys should be capitalized, and should be unique within the namespace.

If we want to localize specific user-facing strings that are only used in a specific component, we can use the `public` namespace. We don't actually need to specify the namespace in this case for the `t` function, since we set this as the default namespace in `src/i18n/config.ts`:

```tsx
<div>
{t(`AboutDescription.VERSION`)}
</div>
```
`locales/en/public.json`
```json
...
"AboutDescription": {
"BUGS": "Bugs",
"FILE_A_REPORT": "File a Report",
"VERSION": "some version!"
},
...
```

To run unit tests using Jest that use a translation, but we want to test the value, use the `testT` function from `src/test/Common.tsx`:

e.g.
```tsx
expect(screen.getByText(testT('CRYOSTAT_TRADEMARK', { ns: 'common' }))).toBeInTheDocument();
```
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,39 +72,66 @@ In this case, API requests are intercepted and handled by [Mirage JS](https://mi
## TEST

### Run the unit tests
```
```bash
$ yarn test
```

### Run the integration tests
```bash
$ yarn itest:preview
```

Refer to [TESTING.md](TESTING.md) for more details about tests.

### Run the linter
[ESLint](https://eslint.org/) is a linter that checks for code quality and style. Configuration can be found in `.eslintrc`.

The `ESLint` job runs on every pull request, and will fail if there are any ESLint errors. Warnings will not fail the job.

To fix this, run:
```bash
$ yarn eslint:apply
```
You can also run `yarn eslint:check` to see if there are any ESLint issues without applying the fixes.

To run a development server with ESLint enabled in hot-reload mode, run:
```bash
$ yarn start:dev:lint
```

With this command, ESLint will run on every file change, and will show ESLint errors/warnings in the terminal.

### Run the code formatter

Prettier is a code formatter that makes sure that all code is formatted the same way. Configuration can be found in `.prettierrc`. There is a `prettierignore` file that tells Prettier to ignore certain files.

The license header checking job makes sure that all files have the correct license header. The npm package can be found [here](https://www.npmjs.com/package/license-check-and-add). The license header can be found in `LICENSE`. The `license-check-and-add` configuration can be found in `license-config.json`.

The `Format` job runs on every pull request, and will fail if the code is not formatted correctly, or if some licenses have not been added to some files.

To fix this, format the code:
```bash
$ yarn format:apply
```
```
You can also run `yarn format:check` to see if there are any formatting issues without applying the formatting.

### Inspect the bundle size

```
```bash
$ yarn bundle-profile:analyze
```

## LOCALIZATION

To generate translation entries for texts in the app, run:

```
yarn localize
```bash
$ yarn localize
```

The extraction tool is [`i18next-parser`](https://www.npmjs.com/package/i18next-parser), which statically finds and exports translation entries, meaning `i18next-parser` does not run code and requires explicit values. See more [details](https://github.com/i18next/i18next-parser#caveats
).

To workaround this, specify static values in `i18n.ts` file under any top-level directory below `src/app`. For example, `src/app/Settings/i18n.ts`.

Refer to [LOCALIZATION.md](LOCALIZATION.md) for more details about our localization framework.
6 changes: 6 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,9 @@ You will also need a WebDriver implementation for the specific browser you want
* [Edge WebDriver (for Microsoft Edge)](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)

Then, finally we can run `yarn itest` to run the integration tests, which will open up a fresh browser window and run the tests.

Alternatively, you can start the integration tests immediately without the need for an already up and running dev server, by running
```bash
$ yarn itest:preview
```
This will automatically start a Mirage dev server, run the integration tests on that server, and tear down the server on completion.
3 changes: 1 addition & 2 deletions license-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"stories",
"__mocks__",
"**/.*",
"**/README.md",
"TESTING.md",
"**/*.md",
"**/*.js",
"yarn.lock",
".yarn/**/*",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"eslint:check": "eslint --cache --ext .ts,.tsx,.js ./src/",
"eslint:apply": "eslint --cache --ext .ts,.tsx,.js --fix ./src/",
"itest:preview": "HEADLESS_BROWSER=true concurrently -c 'auto' 'yarn:start:dev:preview --no-open' 'yarn:itest' --kill-others --success first",
"itest": "wait-on -lt 60000 http://localhost:9091 && jest --testMatch='**/itest/**/*.test.(ts|tsx)'",
"itest": "wait-on -l --httpTimeout 120000 -d 5000 http://localhost:9091 && jest --testMatch='**/itest/**/*.test.(ts|tsx)'",
"license:check": "license-check-and-add check -f license-config.json",
"license:apply": "license-check-and-add add -f license-config.json",
"lint": "concurrently -c 'auto' 'yarn:format' 'yarn:eslint' 'yarn:type-check'",
Expand Down
110 changes: 23 additions & 87 deletions src/itest/Dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,26 @@
* limitations under the License.
*/
import assert from 'assert';
import { By, WebDriver, until } from 'selenium-webdriver';
import {
getElementByCSS,
getElementById,
getElementByLinkText,
getElementByXPath,
selectFakeTarget,
setupBuilder,
} from './util';
import { WebDriver } from 'selenium-webdriver';
import { CardType, Cryostat, Dashboard, setupDriver } from './util';

describe('Dashboard route functionalities', function () {
let driver: WebDriver;
jest.setTimeout(30000);
let dashboard: Dashboard;
let cryostat: Cryostat;
jest.setTimeout(60000);

beforeAll(async function () {
driver = await setupBuilder().build();
await driver.get('http://localhost:9091');
driver = await setupDriver();
cryostat = Cryostat.getInstance(driver);
dashboard = await cryostat.navigateToDashboard();

const skipButton = await driver
.wait(until.elementLocated(By.css('button[data-action="skip"]')), 1000)
.catch(() => null);
if (skipButton) await skipButton.click();

await selectFakeTarget(driver);
await cryostat.skipTour();
await cryostat.selectFakeTarget();
});

afterAll(async function () {
await driver.quit();
await driver.close();
});

it('shows correct route', async function () {
Expand All @@ -51,78 +43,22 @@ describe('Dashboard route functionalities', function () {
});

it('adds a new layout', async function () {
const layoutSelector = await getElementById(driver, 'dashboard-layout-dropdown-toggle');
await layoutSelector.click();

const newLayoutButton = await getElementByXPath(driver, '//button[contains(.,"New Layout")]');
await newLayoutButton.click();

const emptyState = await getElementByCSS(driver, `.pf-c-empty-state__content`);
expect(emptyState).toBeTruthy();
await dashboard.addLayout();
const layoutName = await dashboard.getLayoutName();
assert.equal(layoutName, 'Custom1');
});

it('adds three different cards', async function () {
let finishButton;
const addCardButton = await getElementByCSS(driver, `[aria-label="Add card"]`);
await addCardButton.click();

// click TargetJVMDetails card
const detailsCard = await getElementById(driver, `JvmDetailsCard.CARD_TITLE`);
await detailsCard.click();

finishButton = await getElementByCSS(driver, 'button.pf-c-button.pf-m-primary[type="submit"]');
await finishButton.click();
await addCardButton.click();

// click AutomatedAnalysis card
const aaCard = await driver.findElement(By.id(`AutomatedAnalysisCard.CARD_TITLE`));
await aaCard.click();

finishButton = await getElementByCSS(driver, 'button.pf-c-button.pf-m-primary[type="submit"]');
await finishButton.click(); // next
await finishButton.click(); // finish

await addCardButton.click();

// click MBeanMetrics card
const mbeanCard = await driver.findElement(By.id(`CHART_CARD.MBEAN_METRICS_CARD_TITLE`));
await mbeanCard.click();

finishButton = await getElementByCSS(driver, 'button.pf-c-button.pf-m-primary[type="submit"]');
await finishButton.click(); // next
await finishButton.click(); // finish
});

it('removes all cards', async function () {
let firstCard = await driver.findElement(
By.xpath(`//div[contains(@class, 'pf-l-grid__item')][@style='--pf-l-grid--item--Order: 0;']`)
);
let actionsButton = await firstCard.findElement(By.css('button[aria-label="Actions"]'));
await actionsButton.click();

let removeButton = await getElementByLinkText(driver, 'Remove');
await removeButton.click();

firstCard = await driver.findElement(
By.xpath(`//div[contains(@class, 'pf-l-grid__item')][@style='--pf-l-grid--item--Order: 0;']`)
);
actionsButton = await firstCard.findElement(By.css('button[aria-label="Actions"]'));
await actionsButton.click();

removeButton = await getElementByLinkText(driver, 'Remove');
await removeButton.click();
it('adds three different cards and removes them', async function () {
await dashboard.addCard(CardType.TARGET_JVM_DETAILS);
await dashboard.addCard(CardType.AUTOMATED_ANALYSIS);
await dashboard.addCard(CardType.MBEAN_METRICS_CHART);

firstCard = await driver.findElement(
By.xpath(`//div[contains(@class, 'pf-l-grid__item')][@style='--pf-l-grid--item--Order: 0;']`)
);
actionsButton = await firstCard.findElement(By.css('button[aria-label="Actions"]'));
await actionsButton.click();
assert.equal((await dashboard.getCards()).length, 3);

removeButton = await getElementByLinkText(driver, 'Remove');
await removeButton.click();
while ((await dashboard.getCards()).length > 0) {
await dashboard.removeCard();
}

// check all cards are removed
const emptyState = await getElementByCSS(driver, `.pf-c-empty-state__content`);
expect(emptyState).toBeTruthy();
assert.ok(await dashboard.isEmpty());
});
});
Loading

0 comments on commit 0b61f98

Please sign in to comment.