You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2022/05/10 17:06:37 UTC
[trafficcontrol] branch master updated: Add Various E2E TPv2 DS Tests (#6805)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 866d7a169d Add Various E2E TPv2 DS Tests (#6805)
866d7a169d is described below
commit 866d7a169d9291c925446013a75f3d7f7c39c10b
Author: Steve Hamrick <sh...@users.noreply.github.com>
AuthorDate: Tue May 10 11:06:30 2022 -0600
Add Various E2E TPv2 DS Tests (#6805)
* Add DS e2e tests
* Run without PR
* Cleanup
* Dont use silly ids, actually create the data before test runs
* Lint fix
* Code review changes
* Allow tests to be ran multiple times
* Dont define custom types, use module augmentation
* Dont call login or end session in each suite
* Wait for callback
* Lint fix
---
.github/workflows/tpv2.yml | 2 +
.../traffic-portal/nightwatch/globals/globals.ts | 158 +++++++++++++++++++--
.../traffic-portal/nightwatch/globals/index.ts | 25 ----
.../traffic-portal/nightwatch/nightwatch.conf.js | 2 +
.../nightwatch/page_objects/common.ts | 30 ++++
.../nightwatch/page_objects/deliveryServiceCard.ts | 77 ++++++++++
.../page_objects/deliveryServiceDetail.ts | 60 ++++++++
.../deliveryServiceInvalidationJobs.ts | 32 +++++
.../nightwatch/page_objects/login.ts | 20 +--
.../traffic-portal/nightwatch/tests/ds/ds.card.ts | 29 ++++
.../nightwatch/tests/ds/ds.details.spec.ts | 51 +++++++
.../nightwatch/tests/ds/ds.invalidate.spec.ts | 63 ++++++++
.../traffic-portal/nightwatch/tests/login.spec.ts | 50 -------
.../nightwatch/tests/login/login.spec.ts | 40 ++++++
.../nightwatch/tests/servers.spec.ts | 34 -----
.../nightwatch/tests/servers/servers.spec.ts | 37 +++++
.../nightwatch/tests/{ => users}/users.spec.ts | 27 ++--
.../traffic-portal/nightwatch/tsconfig.e2e.json | 3 +-
experimental/traffic-portal/package-lock.json | 65 +++++++--
experimental/traffic-portal/package.json | 4 +
.../deliveryservice/deliveryservice.component.html | 6 +-
.../src/app/core/ds-card/ds-card.component.html | 2 +-
.../invalidation-jobs.component.html | 2 +-
23 files changed, 659 insertions(+), 160 deletions(-)
diff --git a/.github/workflows/tpv2.yml b/.github/workflows/tpv2.yml
index 066f55a944..7968059e98 100644
--- a/.github/workflows/tpv2.yml
+++ b/.github/workflows/tpv2.yml
@@ -21,6 +21,8 @@ env:
ALPINE_VERSION: sha256:08d6ca16c60fe7490c03d10dc339d9fd8ea67c6466dea8d558526b1330a85930
on:
+ push:
+ create:
pull_request:
paths:
- experimental/traffic-portal/**
diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts b/experimental/traffic-portal/nightwatch/globals/globals.ts
index a624e86707..e6d4691e33 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -12,20 +12,156 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {type NightwatchGlobals} from "nightwatch";
+import * as https from "https";
-/**
- * Defines the configuration used for the testing environment
- */
-export interface GlobalConfig extends NightwatchGlobals {
- adminPass: string;
- adminUser: string;
- trafficOpsURL: string;
+import axios, {AxiosError} from "axios";
+import {NightwatchBrowser} from "nightwatch";
+import type {CommonPageObject} from "nightwatch/page_objects/common";
+import type {DeliveryServiceCardPageObject} from "nightwatch/page_objects/deliveryServiceCard";
+import type {DeliveryServiceDetailPageObject} from "nightwatch/page_objects/deliveryServiceDetail";
+import type {DeliveryServiceInvalidPageObject} from "nightwatch/page_objects/deliveryServiceInvalidationJobs";
+import type {LoginPageObject} from "nightwatch/page_objects/login";
+import type {ServersPageObject} from "nightwatch/page_objects/servers";
+import type {UsersPageObject} from "nightwatch/page_objects/users";
+import {
+ CDN,
+ GeoLimit, GeoProvider, LoginRequest,
+ Protocol,
+ RequestDeliveryService,
+ ResponseCDN,
+ ResponseDeliveryService
+} from "trafficops-types";
+
+declare module "nightwatch" {
+ /**
+ * Defines the global nightwatch browser type with our types mixed in.
+ */
+ export interface NightwatchCustomPageObjects {
+ common: () => CommonPageObject;
+ deliveryServiceCard: () => DeliveryServiceCardPageObject;
+ deliveryServiceDetail: () => DeliveryServiceDetailPageObject;
+ deliveryServiceInvalidationJobs: () => DeliveryServiceInvalidPageObject;
+ login: () => LoginPageObject;
+ servers: () => ServersPageObject;
+ users: () => UsersPageObject;
+ }
+
+ /**
+ * Defines the additional types needed for the test environment.
+ */
+ export interface NightwatchGlobals {
+ adminPass: string;
+ adminUser: string;
+ trafficOpsURL: string;
+ apiVersion: string;
+ uniqueString: string;
+ }
}
-const config = {
+
+const globals = {
adminPass: "twelve12",
adminUser: "admin",
- trafficOpsURL: "https://localhost:6443"
+ afterEach: (browser: NightwatchBrowser, done: () => void): void => {
+ browser.end(() => {
+ done();
+ });
+ },
+ apiVersion: "4.0",
+ before: async (done: () => void): Promise<void> => {
+ const apiUrl = `${globals.trafficOpsURL}/api/${globals.apiVersion}`;
+ const client = axios.create({
+ httpsAgent: new https.Agent({
+ rejectUnauthorized: false
+ })
+ });
+ let accessToken = "";
+ const loginReq: LoginRequest = {
+ p: globals.adminPass,
+ u: globals.adminUser
+ };
+ try {
+ const resp = await client.post(`${apiUrl}/user/login`, JSON.stringify(loginReq));
+ if(resp.headers["set-cookie"]) {
+ for (const cookie of resp.headers["set-cookie"]) {
+ if(cookie.indexOf("access_token") > -1) {
+ accessToken = cookie;
+ break;
+ }
+ }
+ }
+ } catch (e) {
+ console.error((e as AxiosError).message);
+ throw e;
+ }
+ if(accessToken === "") {
+ console.error("Access token is not set");
+ return Promise.reject();
+ }
+ client.defaults.headers.common = { Cookie: accessToken };
+
+ const cdn: CDN = {
+ dnssecEnabled: false, domainName: `tests${globals.uniqueString}.com`, name: `testCDN${globals.uniqueString}`
+ };
+ let respCDN: ResponseCDN;
+ try {
+ let resp = await client.post(`${apiUrl}/cdns`, JSON.stringify(cdn));
+ respCDN = resp.data.response;
+
+ const ds: RequestDeliveryService = {
+ active: false,
+ cacheurl: null,
+ cdnId: respCDN.id,
+ displayName: `test DS${globals.uniqueString}`,
+ dscp: 0,
+ ecsEnabled: false,
+ edgeHeaderRewrite: null,
+ fqPacingRate: null,
+ geoLimit: GeoLimit.NONE,
+ geoProvider: GeoProvider.MAX_MIND,
+ httpBypassFqdn: null,
+ infoUrl: null,
+ initialDispersion: 1,
+ ipv6RoutingEnabled: false,
+ logsEnabled: false,
+ maxOriginConnections: 0,
+ maxRequestHeaderBytes: 0,
+ midHeaderRewrite: null,
+ missLat: 0,
+ missLong: 0,
+ multiSiteOrigin: false,
+ orgServerFqdn: "http://test.com",
+ profileId: 1,
+ protocol: Protocol.HTTP,
+ qstringIgnore: 0,
+ rangeRequestHandling: 0,
+ regionalGeoBlocking: false,
+ remapText: null,
+ routingName: "test",
+ signed: false,
+ tenantId: 1,
+ typeId: 1,
+ xmlId: `testDS${globals.uniqueString}`
+ };
+ resp = await client.post(`${apiUrl}/deliveryservices`, JSON.stringify(ds));
+ const respDS: ResponseDeliveryService = resp.data.response[0];
+ console.log(`Successfully created DS '${respDS.displayName}'`);
+ } catch(e) {
+ console.error((e as AxiosError).message);
+ throw e;
+ }
+ done();
+ },
+ beforeEach: (browser: NightwatchBrowser, done: () => void): void => {
+ browser.page.login()
+ .navigate().section.loginForm
+ .loginAndWait(browser.globals.adminUser, browser.globals.adminPass);
+ // This ensures that we call done after loginAndWait is finished
+ browser.pause(1, () => {
+ done();
+ });
+ },
+ trafficOpsURL: "https://localhost:6443",
+ uniqueString: new Date().getTime().toString()
};
-module.exports = config;
+module.exports = globals;
diff --git a/experimental/traffic-portal/nightwatch/globals/index.ts b/experimental/traffic-portal/nightwatch/globals/index.ts
deleted file mode 100644
index cf939cab87..0000000000
--- a/experimental/traffic-portal/nightwatch/globals/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-* 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 type { NightwatchBrowser } from "nightwatch";
-
-import type { GlobalConfig } from "./globals";
-
-/**
- * A test suite is a mapping of test descriptions to the functions that
- * implement the thereby described test.
- */
-export interface TestSuite {
- [description: string]: (browser: NightwatchBrowser & {globals: GlobalConfig}) => (void | Promise<void>);
-}
diff --git a/experimental/traffic-portal/nightwatch/nightwatch.conf.js b/experimental/traffic-portal/nightwatch/nightwatch.conf.js
index eb6cf1d1fe..b9adc3cd4c 100644
--- a/experimental/traffic-portal/nightwatch/nightwatch.conf.js
+++ b/experimental/traffic-portal/nightwatch/nightwatch.conf.js
@@ -67,6 +67,7 @@ module.exports = {
]
}
},
+ enable_fail_fast: false,
extends: "chrome"
},
@@ -76,6 +77,7 @@ module.exports = {
browserName: "chrome"
},
disable_error_log: false,
+ enable_fail_fast: true,
launch_url: "http://localhost:4200",
output_folder: "nightwatch/junit",
screenshots: {
diff --git a/experimental/traffic-portal/nightwatch/page_objects/common.ts b/experimental/traffic-portal/nightwatch/page_objects/common.ts
new file mode 100644
index 0000000000..7bf6f52ede
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/common.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 {EnhancedPageObject} from "nightwatch";
+
+/**
+ * Defines the type for the common PO
+ */
+export type CommonPageObject = EnhancedPageObject<{}, typeof commonPageObject.elements>;
+
+const commonPageObject = {
+ elements: {
+ snackbarEle: {
+ selector: "simple-snack-bar"
+ }
+ }
+};
+
+export default commonPageObject;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceCard.ts b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceCard.ts
new file mode 100644
index 0000000000..40abb4fe6c
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceCard.ts
@@ -0,0 +1,77 @@
+/*
+* 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 {
+ EnhancedElementInstance,
+ EnhancedPageObject, EnhancedSectionInstance, NightwatchAPI
+} from "nightwatch";
+
+/**
+ * Defines the commands for the loginForm section
+ */
+interface DeliveryServiceCardCommands extends EnhancedSectionInstance, EnhancedElementInstance<EnhancedPageObject> {
+ expandDS(xmlId: string): Promise<boolean>;
+ viewDetails(xmlId: string): Promise<boolean>;
+}
+
+/**
+ * Defines the loginForm section
+ */
+type DeliveryServiceCardSection = EnhancedSectionInstance<DeliveryServiceCardCommands,
+ typeof deliveryServiceCardPageObject.sections.cards.elements>;
+
+/**
+ * Define the type for our PO
+ */
+export type DeliveryServiceCardPageObject = EnhancedPageObject<{}, {}, { cards: DeliveryServiceCardSection }>;
+
+const deliveryServiceCardPageObject = {
+ api: {} as NightwatchAPI,
+ sections: {
+ cards: {
+ commands: {
+ async expandDS(xmlId: string): Promise<boolean> {
+ return new Promise((resolve, reject) => {
+ this.click("css selector", `mat-card#${xmlId}`, result => {
+ if (result.status === 1) {
+ reject(new Error(`Unable to find by css mat-card#${xmlId}`));
+ return;
+ }
+ this.waitForElementVisible(`mat-card#${xmlId} mat-card-content > div`,
+ undefined, undefined, undefined, () => {
+ resolve(true);
+ });
+ });
+ });
+ },
+ async viewDetails(xmlId: string): Promise<boolean> {
+ await this.expandDS(xmlId);
+ return new Promise((resolve) => {
+ this.click("css selector", `mat-card#${xmlId} mat-card-actions > a`, () => {
+ browser.assert.urlContains("deliveryservice");
+ resolve(true);
+ });
+ });
+ }
+ } as DeliveryServiceCardCommands,
+ elements: {
+ },
+ selector: "article#deliveryservices"
+ }
+ },
+ url(): string {
+ return `${this.api.launchUrl}/core`;
+ }
+};
+
+export default deliveryServiceCardPageObject;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceDetail.ts b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceDetail.ts
new file mode 100644
index 0000000000..6b374bd2cd
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceDetail.ts
@@ -0,0 +1,60 @@
+/*
+* 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 {
+ EnhancedPageObject, EnhancedSectionInstance
+} from "nightwatch";
+
+/**
+ * Define the type for our PO
+ */
+export type DeliveryServiceDetailPageObject = EnhancedPageObject<{}, typeof deliveryServiceDetailPageObject.elements,
+{ dateInputForm: EnhancedSectionInstance<{}, typeof deliveryServiceDetailPageObject.sections.dateInputForm.elements> }>;
+
+const deliveryServiceDetailPageObject = {
+ elements: {
+ bandwidthChart: {
+ selector: "canvas#bandwidthData"
+ },
+ invalidateJobs: {
+ selector: "a#invalidate"
+ },
+ tpsChart: {
+ selector: "canvas#tpsChartData"
+ },
+ },
+ sections: {
+ dateInputForm: {
+ elements: {
+ fromDate: {
+ selector: "input[name='fromdate']"
+ },
+ fromTime: {
+ selector: "input[name='fromtime']"
+ },
+ refreshBtn: {
+ selector: "button[name='timespanRefresh']"
+ },
+ toDate: {
+ selector: "input[name='todate']"
+ },
+ toTime: {
+ selector: "input[name='totime']"
+ }
+ },
+ selector: "form[name='timespan']"
+ }
+ }
+};
+
+export default deliveryServiceDetailPageObject;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceInvalidationJobs.ts b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceInvalidationJobs.ts
new file mode 100644
index 0000000000..e9e3f80812
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/deliveryServiceInvalidationJobs.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 {
+ EnhancedPageObject
+} from "nightwatch";
+
+/**
+ * Define the type for our PO
+ */
+export type DeliveryServiceInvalidPageObject = EnhancedPageObject<{}, typeof deliveryServiceInvalidPageObject.elements>;
+
+const deliveryServiceInvalidPageObject = {
+ elements: {
+ addButton: {
+ selector: "button#new"
+ }
+ }
+};
+
+export default deliveryServiceInvalidPageObject;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/login.ts b/experimental/traffic-portal/nightwatch/page_objects/login.ts
index f1ab698e96..69a83c8960 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/login.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/login.ts
@@ -22,6 +22,7 @@ import {
interface LoginFormSectionCommands extends EnhancedSectionInstance, EnhancedElementInstance<EnhancedPageObject> {
fillOut(username: string, password: string): this;
login(username: string, password: string): this;
+ loginAndWait(username: string, password: string): this;
}
/**
@@ -32,27 +33,28 @@ type LoginFormSection = EnhancedSectionInstance<LoginFormSectionCommands, typeof
/**
* Define the type for our PO
*/
-export type LoginPageObject = EnhancedPageObject<{}, typeof loginPageObject.elements, { loginForm: LoginFormSection }>;
+export type LoginPageObject = EnhancedPageObject<{}, {}, { loginForm: LoginFormSection }>;
const loginPageObject = {
api: {} as NightwatchAPI,
- elements: {
- snackbarEle: {
- selector: "simple-snack-bar"
- }
- },
sections: {
loginForm: {
commands: {
- fillOut(username: string, password: string): LoginFormSectionCommands {
- return this
+ fillOut(username: string, password: string): LoginFormSectionCommands {
+ return this
.setValue("@usernameTxt", username)
.setValue("@passwordTxt", password);
},
login(username: string, password: string): LoginFormSectionCommands {
- return this.fillOut(username, password)
+ return this.fillOut(username, password)
.click("@loginBtn");
},
+ loginAndWait(username: string, password: string): LoginFormSectionCommands {
+ const ret = this.login(username, password);
+ browser.page.common()
+ .assert.containsText("@snackbarEle", "Success");
+ return ret;
+ }
} as LoginFormSectionCommands,
elements: {
clearBtn: {
diff --git a/experimental/traffic-portal/nightwatch/tests/ds/ds.card.ts b/experimental/traffic-portal/nightwatch/tests/ds/ds.card.ts
new file mode 100644
index 0000000000..5c27e8f7ab
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/ds/ds.card.ts
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+describe("DS Card Spec", () => {
+ it("Verify expand test", async (): Promise<void> => {
+ await browser.page.deliveryServiceCard()
+ .navigate()
+ .section.cards
+ .expandDS(`testDS${browser.globals.uniqueString}`);
+ });
+
+ it("Verify detail test", async (): Promise<void> => {
+ await browser.page.deliveryServiceCard()
+ .navigate()
+ .section.cards
+ .viewDetails(`testDS${browser.globals.uniqueString}`);
+ });
+});
diff --git a/experimental/traffic-portal/nightwatch/tests/ds/ds.details.spec.ts b/experimental/traffic-portal/nightwatch/tests/ds/ds.details.spec.ts
new file mode 100644
index 0000000000..ed240a27a2
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/ds/ds.details.spec.ts
@@ -0,0 +1,51 @@
+/*
+* 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.
+*/
+
+describe("DS Detail Spec", () => {
+ beforeEach(() => {
+ browser.page.deliveryServiceCard()
+ .navigate()
+ .section.cards
+ .viewDetails(`testDS${browser.globals.uniqueString}`);
+ });
+
+ it("Verify page test", (): void => {
+ const page = browser.page.deliveryServiceDetail();
+ page.assert.visible("@bandwidthChart")
+ .assert.visible("@tpsChart")
+ .assert.enabled("@invalidateJobs");
+
+ page.section.dateInputForm
+ .assert.enabled("@fromDate")
+ .assert.enabled("@fromTime")
+ .assert.enabled("@toDate")
+ .assert.enabled("@toTime")
+ .assert.enabled("@refreshBtn");
+ });
+
+ it("Default values test", (): void => {
+ const page = browser.page.deliveryServiceDetail();
+ const now = new Date();
+ const nowString = now.toISOString();
+ const date = nowString.split("T")[0];
+ let time = nowString.split("T")[1].substring(0, 5);
+ time = `${(+time.split(":")[0] - now.getTimezoneOffset()/60).toString().padStart(2, "0")}:${time.split(":")[1]}`;
+
+ page.section.dateInputForm
+ .assert.value("@fromDate", date)
+ .assert.value("@fromTime", "00:00")
+ .assert.value("@toDate", date)
+ .assert.value("@toTime", time);
+ });
+});
diff --git a/experimental/traffic-portal/nightwatch/tests/ds/ds.invalidate.spec.ts b/experimental/traffic-portal/nightwatch/tests/ds/ds.invalidate.spec.ts
new file mode 100644
index 0000000000..a90883e49f
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/ds/ds.invalidate.spec.ts
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+describe("DS Invalidation Jobs Spec", () => {
+ beforeEach(() => {
+ browser.page.deliveryServiceCard()
+ .navigate()
+ .section.cards
+ .viewDetails(`testDS${browser.globals.uniqueString}`);
+ browser.page.deliveryServiceDetail()
+ .click("@invalidateJobs")
+ .assert.urlContains("invalidation-jobs");
+ });
+
+ it("Verify page", () => {
+ browser.page.deliveryServiceInvalidationJobs()
+ .assert.enabled("@addButton");
+ });
+
+ it("Manage Job", async () => {
+ const page = browser.page.deliveryServiceInvalidationJobs();
+ const common = browser.page.common();
+ page
+ .click("@addButton");
+ const startDate = new Date();
+ startDate.setDate(startDate.getDate() + 1);
+ browser.waitForElementVisible("tp-new-invalidation-job-dialog")
+ .assert.value("input[name='startDate']", startDate.toLocaleDateString())
+ .setValue("input[name='regexp']", "/invalidateMe")
+ .click("button#submit");
+ common
+ .assert.containsText("@snackbarEle", "created")
+ .click("simple-snack-bar button");
+ page.assert.visible({index: 0, selector: "li.invalidation-job"})
+ .assert.enabled({index: 0, selector: "li.invalidation-job button"})
+ .assert.enabled({index: 1, selector: "li.invalidation-job button"});
+ page
+ .click({index: 0, selector: "li.invalidation-job button"});
+ browser.waitForElementVisible("tp-new-invalidation-job-dialog")
+ .assert.value("input[name='startDate']", startDate.toLocaleDateString())
+ .assert.value("input[name='regexp']", "invalidateMe")
+ .setValue("input[name='regexp']", "/invalidateMe2")
+ .click("button#submit");
+ common
+ .assert.containsText("@snackbarEle", "created")
+ .click("simple-snack-bar button");
+ page
+ .click({index: 1, selector: "li.invalidation-job button"});
+ common
+ .assert.containsText("@snackbarEle", "was deleted");
+ });
+});
diff --git a/experimental/traffic-portal/nightwatch/tests/login.spec.ts b/experimental/traffic-portal/nightwatch/tests/login.spec.ts
deleted file mode 100644
index ec92912df0..0000000000
--- a/experimental/traffic-portal/nightwatch/tests/login.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-* 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 type { TestSuite } from "../globals";
-import type { LoginPageObject } from "../page_objects/login";
-
-const suite: TestSuite = {
- "Clear form test": browser => {
- const page: LoginPageObject = browser.page.login();
- page.navigate()
- .section.loginForm
- .fillOut("test", "asdf")
- .click("@clearBtn")
- .assert.containsText("@usernameTxt", "")
- .assert.containsText("@passwordTxt", "")
- .end();
- },
- "Incorrect password test": browser => {
- const page: LoginPageObject = browser.page.login();
- page.navigate()
- .section.loginForm
- .login("test", "asdf")
- .assert.value("@usernameTxt", "test")
- .assert.value("@passwordTxt", "asdf");
- page
- .assert.containsText("@snackbarEle", "Invalid")
- .end();
- },
- "Login test": browser => {
- const page: LoginPageObject = browser.page.login();
- page.navigate()
- .section.loginForm
- .login(browser.globals.adminUser, browser.globals.adminPass)
- .parent
- .assert.containsText("@snackbarEle", "Success")
- .end();
- }
-};
-
-export default suite;
diff --git a/experimental/traffic-portal/nightwatch/tests/login/login.spec.ts b/experimental/traffic-portal/nightwatch/tests/login/login.spec.ts
new file mode 100644
index 0000000000..f8ae5ad224
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/login/login.spec.ts
@@ -0,0 +1,40 @@
+/*
+* 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.
+*/
+
+describe("Login Spec", () => {
+
+ it("Clear form test", () => {
+ browser.page.login()
+ .navigate().section.loginForm
+ .fillOut("test", "asdf")
+ .click("@clearBtn")
+ .assert.containsText("@usernameTxt", "")
+ .assert.containsText("@passwordTxt", "");
+ });
+ it("Incorrect password test", () => {
+ browser.page.login()
+ .navigate().section.loginForm
+ .login("test", "asdf")
+ .assert.value("@usernameTxt", "test")
+ .assert.value("@passwordTxt", "asdf");
+ browser.page.common()
+ .assert.containsText("@snackbarEle", "Invalid");
+ });
+ it("Login test", () => {
+ browser.page.login()
+ .navigate()
+ .section.loginForm
+ .loginAndWait(browser.globals.adminUser, browser.globals.adminPass);
+ });
+});
diff --git a/experimental/traffic-portal/nightwatch/tests/servers.spec.ts b/experimental/traffic-portal/nightwatch/tests/servers.spec.ts
deleted file mode 100644
index 301683b01e..0000000000
--- a/experimental/traffic-portal/nightwatch/tests/servers.spec.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-* 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 type { TestSuite } from "../globals";
-import type { LoginPageObject } from "../page_objects/login";
-import type { ServersPageObject } from "../page_objects/servers";
-
-const suite: TestSuite = {
- "Filter by hostname": async browser => {
- const username = browser.globals.adminUser;
- const password = browser.globals.adminPass;
-
- const loginPage: LoginPageObject = browser.page.login();
- loginPage.navigate().section.loginForm.login(username, password);
-
- const page: ServersPageObject = browser.waitForElementPresent("main").page.servers().navigate();
- page.pause(4000);
- let tbl = page.waitForElementPresent("input[name=fuzzControl]").section.serversTable;
- tbl = tbl.searchText("edge");
- tbl.parent.assert.urlContains("search=edge").end();
- }
-};
-
-export default suite;
diff --git a/experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts b/experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts
new file mode 100644
index 0000000000..606a459795
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/servers/servers.spec.ts
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+/*
+* 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.
+*/
+
+describe("Servers Spec", () => {
+ it("Filter by hostname", async () => {
+ const page = browser.page.servers();
+ page.navigate()
+ .waitForElementPresent("input[name=fuzzControl]");
+ page.section.serversTable.searchText("edge");
+ page.assert.urlContains("search=edge");
+ });
+});
diff --git a/experimental/traffic-portal/nightwatch/tests/users.spec.ts b/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
similarity index 73%
rename from experimental/traffic-portal/nightwatch/tests/users.spec.ts
rename to experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
index a74ee7f00b..fe15afe2d9 100644
--- a/experimental/traffic-portal/nightwatch/tests/users.spec.ts
+++ b/experimental/traffic-portal/nightwatch/tests/users/users.spec.ts
@@ -11,26 +11,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type { TestSuite } from "../globals";
-import { LoginPageObject } from "../page_objects/login";
-import type { UsersPageObject } from "../page_objects/users";
+import type { UsersPageObject } from "nightwatch/page_objects/users";
-const suite: TestSuite = {
- "Filter by username": async browser => {
+describe("Users Spec", () => {
+ it("Filter by username", async () => {
const username = browser.globals.adminUser;
- const password = browser.globals.adminPass;
-
- const loginPage: LoginPageObject = browser.page.login();
- loginPage.navigate().section.loginForm.login(username, password);
const page: UsersPageObject = browser.waitForElementPresent("main").page.users();
- let tbl = page.navigate().waitForElementPresent(".ag-row").section.usersTable;
+ page.navigate()
+ .waitForElementPresent(".ag-row");
+ let tbl = page.section.usersTable;
if (! await tbl.getColumnState("Username")) {
tbl = tbl.toggleColumn("Username");
}
tbl = tbl.searchText(username);
- tbl.parent.assert.urlContains(`search=${username}`);
+ page.assert.urlContains(`search=${username}`);
tbl.api.elements("css selector", ".ag-row:not(.ag-hidden .ag-row)",
result => {
@@ -38,11 +34,10 @@ const suite: TestSuite = {
browser.assert.equal(true, false, `failed to select ag-grid rows: ${result.value.message}`);
return;
}
- browser.assert.equal(result.value.length, 1)
- .end();
+ browser.assert.equal(result.value.length, 1);
}
);
- },
+ });
// Uncomment when user details page exists
// "View user details": browser => {
// const username = browser.globals.adminUser;
@@ -57,6 +52,4 @@ const suite: TestSuite = {
// const userRow = tbl.parent.api.moveToElement(".ag-row:not(.ag-hidden .ag-row)", 2, 2, 100, {pointer: 0, viewport: 0});
// userRow.mouseButtonClick("right").click("button[name=View-User-Details]").assert.urlContains(username);
// }
-};
-
-export default suite;
+});
diff --git a/experimental/traffic-portal/nightwatch/tsconfig.e2e.json b/experimental/traffic-portal/nightwatch/tsconfig.e2e.json
index b7b2cca528..69eac9f912 100644
--- a/experimental/traffic-portal/nightwatch/tsconfig.e2e.json
+++ b/experimental/traffic-portal/nightwatch/tsconfig.e2e.json
@@ -6,7 +6,8 @@
"target": "es5",
"types": [
"node",
- "nightwatch"
+ "nightwatch",
+ "mocha"
],
"rootDirs": ["./page_objects", "./tests", "./globals"]
}
diff --git a/experimental/traffic-portal/package-lock.json b/experimental/traffic-portal/package-lock.json
index 39d300465c..4c7bf4a7fc 100644
--- a/experimental/traffic-portal/package-lock.json
+++ b/experimental/traffic-portal/package-lock.json
@@ -49,10 +49,12 @@
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
+ "@types/mocha": "^9.1.1",
"@types/nightwatch": "2.0.1",
"@types/node": "^14.17.34",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
+ "axios": "^0.27.2",
"chromedriver": "^100.0.0",
"codelyzer": "^6.0.0",
"eslint": "^8.2.0",
@@ -67,6 +69,7 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"nightwatch": "2.0.0-beta.3",
+ "trafficops-types": "^3.1.0-beta-6",
"ts-node": "~8.3.0",
"typescript": "^4.5.4"
},
@@ -4304,6 +4307,12 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
+ "node_modules/@types/mocha": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz",
+ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==",
+ "dev": true
+ },
"node_modules/@types/nightwatch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/nightwatch/-/nightwatch-2.0.1.tgz",
@@ -5525,12 +5534,13 @@
}
},
"node_modules/axios": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
- "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+ "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dev": true,
"dependencies": {
- "follow-redirects": "^1.14.4"
+ "follow-redirects": "^1.14.9",
+ "form-data": "^4.0.0"
}
},
"node_modules/axobject-query": {
@@ -6389,6 +6399,15 @@
"node": ">=10"
}
},
+ "node_modules/chromedriver/node_modules/axios": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
+ "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "dev": true,
+ "dependencies": {
+ "follow-redirects": "^1.14.4"
+ }
+ },
"node_modules/ci-info": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
@@ -17073,6 +17092,12 @@
"node": ">=12"
}
},
+ "node_modules/trafficops-types": {
+ "version": "3.1.0-beta-6",
+ "resolved": "https://registry.npmjs.org/trafficops-types/-/trafficops-types-3.1.0-beta-6.tgz",
+ "integrity": "sha512-CbsA8rdQCxAyBcm/MxIjvQEyYTiMpXlsDlaBTRltEro2aSMYztUf5ieFMCkMG2txi07fT+X9vdQE2f2jrgL2kQ==",
+ "dev": true
+ },
"node_modules/traverse": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
@@ -21371,6 +21396,12 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
+ "@types/mocha": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz",
+ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==",
+ "dev": true
+ },
"@types/nightwatch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/nightwatch/-/nightwatch-2.0.1.tgz",
@@ -22274,12 +22305,13 @@
}
},
"axios": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
- "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+ "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dev": true,
"requires": {
- "follow-redirects": "^1.14.4"
+ "follow-redirects": "^1.14.9",
+ "form-data": "^4.0.0"
}
},
"axobject-query": {
@@ -22936,6 +22968,17 @@
"https-proxy-agent": "^5.0.0",
"proxy-from-env": "^1.1.0",
"tcp-port-used": "^1.0.1"
+ },
+ "dependencies": {
+ "axios": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
+ "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "dev": true,
+ "requires": {
+ "follow-redirects": "^1.14.4"
+ }
+ }
}
},
"ci-info": {
@@ -30961,6 +31004,12 @@
"punycode": "^2.1.1"
}
},
+ "trafficops-types": {
+ "version": "3.1.0-beta-6",
+ "resolved": "https://registry.npmjs.org/trafficops-types/-/trafficops-types-3.1.0-beta-6.tgz",
+ "integrity": "sha512-CbsA8rdQCxAyBcm/MxIjvQEyYTiMpXlsDlaBTRltEro2aSMYztUf5ieFMCkMG2txi07fT+X9vdQE2f2jrgL2kQ==",
+ "dev": true
+ },
"traverse": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
diff --git a/experimental/traffic-portal/package.json b/experimental/traffic-portal/package.json
index 5ebb6953aa..7164ce906a 100644
--- a/experimental/traffic-portal/package.json
+++ b/experimental/traffic-portal/package.json
@@ -34,6 +34,7 @@
"start": "ng serve",
"build": "ng build",
"test": "ng test",
+ "clean": "rm -rf out-tsc nightwatch/junit nightwatch/screens tests_output logs",
"coverage": "ng test --code-coverage",
"test:ci": "ng test --watch=false --browsers=ChromeHeadlessCustom",
"coverage:ci": "ng test --code-coverage --watch=false --browsers=ChromeHeadlessCustom",
@@ -88,10 +89,12 @@
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
+ "@types/mocha": "^9.1.1",
"@types/nightwatch": "2.0.1",
"@types/node": "^14.17.34",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
+ "axios": "^0.27.2",
"chromedriver": "^100.0.0",
"codelyzer": "^6.0.0",
"eslint": "^8.2.0",
@@ -106,6 +109,7 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"nightwatch": "2.0.0-beta.3",
+ "trafficops-types": "^3.1.0-beta-6",
"ts-node": "~8.3.0",
"typescript": "^4.5.4"
},
diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.html b/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.html
index 72653aa3a9..94c253987d 100644
--- a/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.html
+++ b/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.html
@@ -35,14 +35,14 @@ limitations under the License.
<input matInput [formControl]="toTime" type="time" title="To (time)" name="totime">
</mat-form-field>
</div>
- <button mat-raised-button>Refresh</button>
+ <button name="timespanRefresh" mat-raised-button>Refresh</button>
</form>
<mat-divider></mat-divider>
- <canvas linechart chartTitle="Bandwidth of Cache Tiers" [chartDataSets]="bandwidthData" chartYAxisLabel="Bandwidth (Kilobytes Per Second)" chartXAxisLabel="Date/Time" [chartDisplayLegend]="true" chartType="time">
+ <canvas linechart chartTitle="Bandwidth of Cache Tiers" id="bandwidthData" [chartDataSets]="bandwidthData" chartYAxisLabel="Bandwidth (Kilobytes Per Second)" chartXAxisLabel="Date/Time" [chartDisplayLegend]="true" chartType="time">
Your browser does not support canvases or Javascript. Normally, this would be a graph of bandwidth data.
</canvas>
<mat-divider></mat-divider>
- <canvas linechart chartTitle="Transactions at the Edge Tier" [chartDataSets]="tpsChartData" chartYAxisLabel="Transactions Per Second" chartXAxisLabel="Date/Time" [chartDisplayLegend]="true" chartType="time">
+ <canvas linechart chartTitle="Transactions at the Edge Tier" id="tpsChartData" [chartDataSets]="tpsChartData" chartYAxisLabel="Transactions Per Second" chartXAxisLabel="Date/Time" [chartDisplayLegend]="true" chartType="time">
Your browser does not support canvases or Javascript. Normally, this would be a graph of transaction data.
</canvas>
<a mat-fab id="invalidate" routerLink="/core/deliveryservice/{{deliveryservice.id}}/invalidation-jobs" title="invalidate content" aria-label="invalidate content">
diff --git a/experimental/traffic-portal/src/app/core/ds-card/ds-card.component.html b/experimental/traffic-portal/src/app/core/ds-card/ds-card.component.html
index 323f9edec3..4113699c93 100644
--- a/experimental/traffic-portal/src/app/core/ds-card/ds-card.component.html
+++ b/experimental/traffic-portal/src/app/core/ds-card/ds-card.component.html
@@ -11,7 +11,7 @@ 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.
-->
-<mat-card [ngClass]="{'inactive': !deliveryService.active, 'open': open, 'first': first, 'last': last}">
+<mat-card id="{{deliveryService.xmlId}}" [ngClass]="{'inactive': !deliveryService.active, 'open': open, 'first': first, 'last': last}">
<mat-card-title-group (click)="toggle()">
<mat-card-title>{{deliveryService.displayName}}{{deliveryService.active ? '' : ' (inactive)'}}
<a href="{{deliveryService.infoUrl}}" *ngIf="deliveryService.infoUrl" class="color-accent-inverted info" rel="noopener" target="_blank" title="More Information"><fa-icon [icon]="infoIcon"></fa-icon></a>
diff --git a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
index e6318028c5..8e475cfc9e 100644
--- a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
+++ b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
@@ -13,7 +13,7 @@ limitations under the License.
-->
<tp-header title="{{deliveryservice ? deliveryservice.displayName : 'Loading'}} - Content Invalidation Jobs"></tp-header>
<ul>
- <li *ngFor="let j of jobs">
+ <li class="invalidation-job" *ngFor="let j of jobs">
<code>{{j.assetUrl}}</code> (active from <time [dateTime]="j.startTime">{{j.startTime | date:'medium'}}</time> to <time [dateTime]="endDate(j)">{{endDate(j) | date:'medium'}})</time>
<button
mat-icon-button