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