diff --git a/LOCALIZATION.md b/LOCALIZATION.md
new file mode 100644
index 000000000..ddc23bdaf
--- /dev/null
+++ b/LOCALIZATION.md
@@ -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 (
+
+ {t('somekey')}
+
+ );
+}
+```
+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
+
+ {t('SOME_COMMON_KEY', { ns: 'common' })}
+
+```
+
+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
+
+ {t('Cancel')}
+
+```
+
+`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
+
+ {t(`AboutDescription.VERSION`)}
+
+```
+`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();
+```
diff --git a/README.md b/README.md
index f3940232b..e882d1c4b 100644
--- a/README.md
+++ b/README.md
@@ -72,27 +72,52 @@ 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
```
@@ -100,11 +125,13 @@ $ yarn bundle-profile:analyze
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.
diff --git a/TESTING.md b/TESTING.md
index 6fc56263b..0c6533d3c 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -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.
diff --git a/license-config.json b/license-config.json
index 328b5f344..92f9764ba 100644
--- a/license-config.json
+++ b/license-config.json
@@ -17,8 +17,7 @@
"stories",
"__mocks__",
"**/.*",
- "**/README.md",
- "TESTING.md",
+ "**/*.md",
"**/*.js",
"yarn.lock",
".yarn/**/*",
diff --git a/package.json b/package.json
index fca912a6f..95cb8f10f 100644
--- a/package.json
+++ b/package.json
@@ -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'",
diff --git a/src/itest/Dashboard.test.ts b/src/itest/Dashboard.test.ts
index dbf7c3ad4..f662f70e4 100644
--- a/src/itest/Dashboard.test.ts
+++ b/src/itest/Dashboard.test.ts
@@ -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 () {
@@ -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());
});
});
diff --git a/src/itest/RecordingWorkflow.test.ts b/src/itest/RecordingWorkflow.test.ts
new file mode 100644
index 000000000..be33400a7
--- /dev/null
+++ b/src/itest/RecordingWorkflow.test.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import assert from 'assert';
+import { RecordingState } from '@app/Shared/Services/Api.service';
+import { WebDriver } from 'selenium-webdriver';
+import { Cryostat, Recordings, setupDriver, sleep } from './util';
+
+describe('Recording workflow steps', function () {
+ let driver: WebDriver;
+ let recordings: Recordings;
+ let cryostat: Cryostat;
+ jest.setTimeout(60000);
+
+ beforeAll(async function () {
+ driver = await setupDriver();
+ cryostat = Cryostat.getInstance(driver);
+ recordings = await cryostat.navigateToRecordings();
+
+ await cryostat.skipTour();
+ await cryostat.selectFakeTarget();
+ });
+
+ afterAll(async function () {
+ await driver.close();
+ });
+
+ it('shows correct route', async function () {
+ const url = await driver.getCurrentUrl();
+ const route = url.split('/').pop();
+ assert.equal('recordings', route);
+ });
+
+ it('creates a new recording', async function () {
+ assert.equal((await recordings.getRecordings()).length, 0);
+ await recordings.createRecording('helloWorld');
+ const active = await recordings.getRecordings();
+ assert.equal(active.length, 1);
+
+ const state = await recordings.getRecordingState(active[0]);
+ assert.equal(state, RecordingState.RUNNING);
+ });
+
+ it('stops a recording', async function () {
+ const active = await recordings.getRecordings();
+ assert.equal(active.length, 1);
+
+ await recordings.stopRecording(active[0]);
+
+ const state = await recordings.getRecordingState(active[0]);
+ assert.equal(state, RecordingState.STOPPED);
+ });
+
+ it('archives a new recording', async function () {
+ const active = await recordings.getRecordings();
+ assert.equal(active.length, 1);
+
+ await recordings.archiveRecording(active[0]);
+ const notif = await cryostat.getLatestNotification();
+
+ assert.equal(notif.title, 'Recording Saved');
+ assert.ok(notif.description.includes('helloWorld'));
+ });
+
+ it('deletes a recording', async function () {
+ const active = await recordings.getRecordings();
+ assert.equal(active.length, 1);
+
+ await recordings.deleteRecording(active[0]);
+ await sleep(10000);
+ assert.equal((await recordings.getRecordings()).length, 0);
+ });
+
+ // TODO: checking UI for download, report generation, label editing
+});
diff --git a/src/itest/util.ts b/src/itest/util.ts
index 42dbb820d..31f0ef753 100644
--- a/src/itest/util.ts
+++ b/src/itest/util.ts
@@ -13,43 +13,265 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Builder, By, WebDriver } from 'selenium-webdriver';
+import { Builder, By, WebDriver, WebElement, WebElementPromise, until } from 'selenium-webdriver';
import firefox from 'selenium-webdriver/firefox';
-export async function selectFakeTarget(driver: WebDriver) {
- const targetName = 'Fake Target';
- const targetSelect = await driver.findElement(By.css(`[aria-label="Options menu"]`));
- await targetSelect.click();
- const targetOption = await driver.findElement(By.xpath(`//*[contains(text(), '${targetName}')]`));
- await targetOption.click();
+const DEFAULT_FIND_ELEMENT_TIMEOUT = 5000;
+
+export function getElementByXPath(driver: WebDriver, xpath: string) {
+ return driver.wait(until.elementLocated(By.xpath(xpath)));
}
-export async function getElementByXPath(driver: WebDriver, xpath: string) {
- const element = await driver.findElement(By.xpath(xpath));
- return element;
+export function getElementByCSS(driver: WebDriver, cssSelector: string) {
+ return driver.wait(until.elementLocated(By.css(cssSelector)));
}
-export async function getElementByCSS(driver: WebDriver, cssSelector: string) {
- const element = await driver.findElement(By.css(cssSelector));
- return element;
+export function getElementById(driver: WebDriver, id: string): WebElementPromise {
+ return driver.wait(until.elementLocated(By.id(id)));
}
-export async function getElementById(driver: WebDriver, id: string) {
- const element = await driver.findElement(By.id(id));
- return element;
+export function getElementByLinkText(driver: WebDriver, linkText: string) {
+ return driver.wait(until.elementLocated(By.linkText(linkText)));
}
-export async function getElementByLinkText(driver: WebDriver, linkText: string) {
- const element = await driver.findElement(By.linkText(linkText));
- return element;
+export function getElementByAttribute(driver: WebDriver, attribute: string, value: string) {
+ return driver.wait(until.elementLocated(By.xpath(`//*[@${attribute}='${value}']`)));
}
-export function setupBuilder(): Builder {
+export async function setupDriver(): Promise {
const headless = process.env.HEADLESS_BROWSER === 'true';
const options = new firefox.Options();
if (headless) {
options.headless();
}
options.setAcceptInsecureCerts(true);
- return new Builder().forBrowser('firefox').setFirefoxOptions(options);
+ options.addArguments('--width=1920', '--height=1080');
+ const driver = new Builder().forBrowser('firefox').setFirefoxOptions(options).build();
+ await driver.manage().setTimeouts({
+ implicit: DEFAULT_FIND_ELEMENT_TIMEOUT,
+ });
+ return driver;
+}
+
+export class Cryostat {
+ private driver: WebDriver;
+ private static instance: Cryostat;
+
+ private constructor(driver: WebDriver) {
+ this.driver = driver;
+ }
+
+ public static getInstance(driver: WebDriver): Cryostat {
+ if (!Cryostat.instance) {
+ Cryostat.instance = new Cryostat(driver);
+ }
+ return Cryostat.instance;
+ }
+
+ async navigateToDashboard(): Promise {
+ await this.driver.get('http://localhost:9091');
+ return new Dashboard(this.driver);
+ }
+
+ async navigateToRecordings(): Promise {
+ await this.driver.get('http://localhost:9091/recordings');
+ return new Recordings(this.driver);
+ }
+
+ async selectFakeTarget() {
+ const targetName = 'Fake Target';
+ const targetSelect = await this.driver.wait(until.elementLocated(By.css(`[aria-label="Options menu"]`)));
+ await targetSelect.click();
+ const targetOption = await this.driver.wait(
+ until.elementLocated(By.xpath(`//*[contains(text(), '${targetName}')]`))
+ );
+ await targetOption.click();
+ }
+
+ async skipTour() {
+ const skipButton = await this.driver
+ .wait(until.elementLocated(By.css('button[data-action="skip"]')))
+ .catch(() => null);
+ if (skipButton) await skipButton.click();
+ }
+
+ async getLatestNotification(): Promise {
+ const latestNotification = await this.driver.wait(
+ until.elementLocated(By.className('pf-c-alert-group pf-m-toast'))
+ );
+ return {
+ title: await getDirectTextContent(
+ this.driver,
+ await latestNotification.findElement(By.css('li:last-of-type .pf-c-alert__title'))
+ ),
+ description: await latestNotification.findElement(By.css('li:last-of-type .pf-c-alert__description')).getText(),
+ };
+ }
+}
+
+// from here: https://stackoverflow.com/a/19040341/22316240
+async function getDirectTextContent(driver: WebDriver, el: WebElement): Promise {
+ return driver.executeScript(
+ `
+ const parent = arguments[0];
+ let child = parent.firstChild;
+ let ret = "";
+ while (child) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ ret += child.textContent;
+ }
+ child = child.nextSibling;
+ }
+ return ret;
+ `,
+ el
+ );
+}
+
+interface ITestNotification {
+ title: string;
+ description: string;
+}
+
+export class Dashboard {
+ private driver: WebDriver;
+
+ constructor(driver: WebDriver) {
+ this.driver = driver;
+ }
+
+ getLayoutName(): Promise {
+ return getElementById(this.driver, 'dashboard-layout-dropdown-toggle').getText();
+ }
+
+ async addLayout() {
+ const layoutSelector = await getElementById(this.driver, 'dashboard-layout-dropdown-toggle');
+ await layoutSelector.click();
+
+ const newLayoutButton = await getElementByXPath(this.driver, '//button[contains(.,"New Layout")]');
+ await newLayoutButton.click();
+ }
+
+ async isEmpty(): Promise {
+ return (await this.getCards()).length == 0;
+ }
+
+ async getCards(): Promise {
+ return await this.driver.findElements(By.className('dashboard-card'));
+ }
+
+ async addCard(cardType: CardType) {
+ const addCardButton = await getElementByCSS(this.driver, `[aria-label="Add card"]`);
+ await addCardButton.click();
+ const twoPartCards = [CardType.AUTOMATED_ANALYSIS, CardType.JFR_METRICS_CHART, CardType.MBEAN_METRICS_CHART];
+
+ switch (cardType) {
+ case CardType.AUTOMATED_ANALYSIS: {
+ const aaCard = await getElementById(this.driver, `AutomatedAnalysisCard.CARD_TITLE`);
+ await aaCard.click();
+ break;
+ }
+ case CardType.JFR_METRICS_CHART:
+ break;
+ case CardType.TARGET_JVM_DETAILS: {
+ const detailsCard = await getElementById(this.driver, `JvmDetailsCard.CARD_TITLE`);
+ await detailsCard.click();
+ break;
+ }
+ case CardType.MBEAN_METRICS_CHART: {
+ const mbeanCard = await getElementById(this.driver, `CHART_CARD.MBEAN_METRICS_CARD_TITLE`);
+ await mbeanCard.click();
+ break;
+ }
+ }
+ const finishButton = await getElementByCSS(this.driver, 'button.pf-c-button.pf-m-primary[type="submit"]');
+ await finishButton.click();
+ if (twoPartCards.includes(cardType)) {
+ await finishButton.click();
+ }
+ }
+
+ async removeCard() {
+ const el: WebElement[] = await this.getCards();
+ let firstCard;
+ if (el.length > 0) {
+ firstCard = el[0];
+ await firstCard.click();
+ } else {
+ return;
+ }
+
+ const actionsButton = await getElementByCSS(this.driver, 'button[aria-label="Actions"]');
+ await actionsButton.click();
+
+ const removeButton = await getElementByLinkText(this.driver, 'Remove');
+ await removeButton.click();
+ }
+}
+
+export class Recordings {
+ private driver: WebDriver;
+
+ constructor(driver: WebDriver) {
+ this.driver = driver;
+ }
+
+ async createRecording(name: string) {
+ const createButton = await getElementByAttribute(this.driver, 'data-quickstart-id', 'recordings-create-btn');
+ await createButton.click();
+
+ // Enter recording name
+ const recordingNameInput = await getElementById(this.driver, 'recording-name');
+ await recordingNameInput.sendKeys(name);
+
+ // Select template
+ await getElementById(this.driver, 'recording-template').sendKeys('Demo Template');
+
+ const submitButton = await getElementByAttribute(this.driver, 'data-quickstart-id', 'crf-create-btn');
+ await submitButton.click();
+ }
+
+ async getRecordings(): Promise {
+ const tableXPath = "//div[@class='recording-table--inner-container pf-c-scroll-inner-wrapper']";
+ return this.driver.findElements(By.xpath(`${tableXPath}//tbody`));
+ }
+
+ async getRecordingState(recording: WebElement): Promise {
+ return recording.findElement(By.xpath(`.//td[@data-label='State']`)).getText();
+ }
+
+ async stopRecording(recording: WebElement) {
+ await recording.findElement(By.xpath(`.//input[@data-quickstart-id='active-recordings-checkbox']`)).click();
+ await getElementByAttribute(this.driver, 'data-quickstart-id', 'recordings-stop-btn').click();
+ }
+
+ async archiveRecording(recording: WebElement) {
+ await recording.findElement(By.xpath(`.//input[@data-quickstart-id='active-recordings-checkbox']`)).click();
+ await getElementByAttribute(this.driver, 'data-quickstart-id', 'recordings-archive-btn').click();
+ }
+
+ async deleteRecording(recording: WebElement) {
+ await recording.findElement(By.xpath(`.//input[@data-quickstart-id='active-recordings-checkbox']`)).click();
+ await getElementByAttribute(this.driver, 'data-quickstart-id', 'recordings-delete-btn').click();
+ // confirm prompt
+ await getElementByXPath(this.driver, `//div[@id='portal-root']//button[contains(text(),'Delete')]`).click();
+ }
+
+ // async addLabel(recording: WebElement, k: string, v: string) {
+ // await recording.findElement(By.xpath(`.//input[@data-quickstart-id='active-recordings-checkbox']`)).click();
+ // }
+
+ // async removeAllLabels(recording: WebElement) {
+ // await recording.findElement(By.xpath(`.//input[@data-quickstart-id='active-recordings-checkbox']`)).click();
+ // }
+}
+
+// utility function for integration test debugging
+export const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export enum CardType {
+ TARGET_JVM_DETAILS,
+ AUTOMATED_ANALYSIS,
+ JFR_METRICS_CHART,
+ MBEAN_METRICS_CHART,
}