Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/devtools/mobileharness/fe/v6/angular/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ export class App implements OnDestroy {
this.showContent = true;
} else if (path === 'hosts/:hostName' && params['hostName']) {
this.showContent = true;
} else if (
path === 'jobs/:jobId/tests/:id' &&
params['id'] &&
params['jobId']
) {
this.showContent = true;
} else {
this.showContent = false;
}
Expand Down
24 changes: 22 additions & 2 deletions src/devtools/mobileharness/fe/v6/angular/app/app_routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import {Routes} from '@angular/router';
import {inject} from '@angular/core';
import {CanActivateFn, Router, Routes} from '@angular/router';
import {DevHarnessPage} from './features/dev_harness/dev_harness_page';
import {DeviceDetailPage} from './features/device_detail/device_detail_page';
import {HostDetail} from './features/host_detail/host_detail';
import {TestDetail} from './features/test_detail/test_detail';

/**
* Router Activation Guard (Sanitizes the browser address bar on initialization or external entrance).
* If a user enters a Test/Job detail page from outside the guest app (e.g. bookmarks or direct links)
* with '?universe=xxx' query param, this guard intercepts the navigation, strips universe from UrlTree,
* and performs a clean redirection.
* Note: This gates external/initial entries, while url_utils manages internal nav link transitions,
* and universeInterceptor shields the backend HTTP request layer.
*/
const stripUniverseGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
if (route.queryParamMap.has('universe')) {
const urlTree = router.parseUrl(state.url);
delete urlTree.queryParams['universe'];
return urlTree;
}
return true;
};

/**
* The application routes.
*/
Expand All @@ -21,8 +40,9 @@ export const routes: Routes = [
component: HostDetail,
},
{
path: 'tests/:id',
path: 'jobs/:jobId/tests/:id',
component: TestDetail,
canActivate: [stripUniverseGuard],
},
{
path: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ export const universeInterceptor: HttpInterceptorFn = (req, next) => {
const urlParams = new URLSearchParams(window.location.search);
const universe = urlParams.get('universe');

if (universe) {
// NETWORK LAYER INTERCEPTION: Exclude '/tests' and '/jobs' requests.
// The gRPC backends for test/job detail (e.g. GetTestRequest) do not support the 'universe' field.
// Injecting it would trigger a backend GFEs / HTTP-transcoding 400 Bad Request error.
// Note: This operates at the HTTP/network request level, while stripUniverseGuard and url_utils
// sanitize the browser navigation URL bar parameters to keep the addresses clean.
if (universe && !req.url.includes('/tests') && !req.url.includes('/jobs')) {
const patchedReq = req.clone({
setParams: {
'universe': universe,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @fileoverview Shared core data models and enums mapping to MobileHarness protos.
*/

/** gRPC-friendly status of a test. */
export enum TestStatus {
TEST_STATUS_UNSPECIFIED = 'TEST_STATUS_UNSPECIFIED',
TEST_STATUS_NEW = 'TEST_STATUS_NEW',
TEST_STATUS_ASSIGNED = 'TEST_STATUS_ASSIGNED',
TEST_STATUS_RUNNING = 'TEST_STATUS_RUNNING',
TEST_STATUS_DONE = 'TEST_STATUS_DONE',
TEST_STATUS_SUSPENDED = 'TEST_STATUS_SUSPENDED',
}

/** gRPC-friendly result of a test. */
export enum TestResult {
TEST_RESULT_UNSPECIFIED = 'TEST_RESULT_UNSPECIFIED',
TEST_RESULT_PASS = 'TEST_RESULT_PASS',
TEST_RESULT_FAIL = 'TEST_RESULT_FAIL',
TEST_RESULT_ERROR = 'TEST_RESULT_ERROR',
TEST_RESULT_TIMEOUT = 'TEST_RESULT_TIMEOUT',
TEST_RESULT_ABORT = 'TEST_RESULT_ABORT',
TEST_RESULT_SKIP = 'TEST_RESULT_SKIP',
}

/** gRPC-friendly status of a job. */
export enum JobStatus {
JOB_STATUS_UNSPECIFIED = 'JOB_STATUS_UNSPECIFIED',
JOB_STATUS_NEW = 'JOB_STATUS_NEW',
JOB_STATUS_RUNNING = 'JOB_STATUS_RUNNING',
JOB_STATUS_DONE = 'JOB_STATUS_DONE',
JOB_STATUS_SUSPENDED = 'JOB_STATUS_SUSPENDED',
JOB_STATUS_ASSIGNED = 'JOB_STATUS_ASSIGNED',
}

/** gRPC-friendly result of a job. */
export enum JobResult {
JOB_RESULT_UNSPECIFIED = 'JOB_RESULT_UNSPECIFIED',
JOB_RESULT_PASS = 'JOB_RESULT_PASS',
JOB_RESULT_FAIL = 'JOB_RESULT_FAIL',
JOB_RESULT_ERROR = 'JOB_RESULT_ERROR',
JOB_RESULT_TIMEOUT = 'JOB_RESULT_TIMEOUT',
JOB_RESULT_ABORT = 'JOB_RESULT_ABORT',
JOB_RESULT_SKIP = 'JOB_RESULT_SKIP',
}

/** gRPC-friendly status of a session. */
export enum SessionStatus {
SESSION_STATUS_UNSPECIFIED = 'SESSION_STATUS_UNSPECIFIED',
SESSION_STATUS_NEW = 'SESSION_STATUS_NEW',
SESSION_STATUS_RUNNING = 'SESSION_STATUS_RUNNING',
SESSION_STATUS_DONE = 'SESSION_STATUS_DONE',
}

/** gRPC-friendly result of a session. */
export enum SessionResult {
SESSION_RESULT_UNSPECIFIED = 'SESSION_RESULT_UNSPECIFIED',
SESSION_RESULT_PASS = 'SESSION_RESULT_PASS',
SESSION_RESULT_FAIL = 'SESSION_RESULT_FAIL',
SESSION_RESULT_ERROR = 'SESSION_RESULT_ERROR',
SESSION_RESULT_TIMEOUT = 'SESSION_RESULT_TIMEOUT',
SESSION_RESULT_ABORT = 'SESSION_RESULT_ABORT',
SESSION_RESULT_SKIP = 'SESSION_RESULT_SKIP',
}

/** Shared definition for troubleshooting error. */
export declare interface ErrorInfo {
message: string;
trace?: string;
}

/** Shared definition for troubleshooting warning. */
export declare interface WarningInfo {
message: string;
trace?: string;
}

/** Shared execution details mapped to the "Execution Details" UI cards. */
export declare interface ExecutionDetails {
user?: string;
actualUser?: string;
cloudLogLink?: string;
createTime: string;
startTime?: string;
endTime?: string;
updateTime?: string;
}

/** Collection container for error entry items. */
export declare interface Errors {
error?: ErrorInfo[];
}

/** Collection container for warning entry items. */
export declare interface Warnings {
warning?: WarningInfo[];
}

/** Troubleshooting layout card details shared by both Jobs and Tests. */
export declare interface Troubleshooting {
warnings?: Warnings;
resultCause?: Errors;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,28 @@
* presentation in the UI, corresponding to test details in mobileharness.
*/

import {
ErrorInfo,
ExecutionDetails,
JobResult,
JobStatus,
SessionResult,
SessionStatus,
TestResult,
TestStatus,
Troubleshooting,
WarningInfo,
} from './common_models';
import {TimeBreakdown} from './timeline';
export type {TimeBreakdown};

/** gRPC-friendly status of a test, decoupled from its result. */
export enum TestStatus {
TEST_STATUS_UNSPECIFIED = 'TEST_STATUS_UNSPECIFIED',
TEST_STATUS_NEW = 'TEST_STATUS_NEW',
TEST_STATUS_ASSIGNED = 'TEST_STATUS_ASSIGNED',
TEST_STATUS_RUNNING = 'TEST_STATUS_RUNNING',
TEST_STATUS_DONE = 'TEST_STATUS_DONE',
TEST_STATUS_SUSPENDED = 'TEST_STATUS_SUSPENDED',
}

/** gRPC-friendly result of a test. */
export enum TestResult {
TEST_RESULT_UNSPECIFIED = 'TEST_RESULT_UNSPECIFIED',
TEST_RESULT_PASS = 'TEST_RESULT_PASS',
TEST_RESULT_FAIL = 'TEST_RESULT_FAIL',
TEST_RESULT_ERROR = 'TEST_RESULT_ERROR',
TEST_RESULT_TIMEOUT = 'TEST_RESULT_TIMEOUT',
TEST_RESULT_ABORT = 'TEST_RESULT_ABORT',
TEST_RESULT_SKIP = 'TEST_RESULT_SKIP',
}

/** gRPC-friendly status of a job, decoupled from its result. */
export enum JobStatus {
JOB_STATUS_UNSPECIFIED = 'JOB_STATUS_UNSPECIFIED',
JOB_STATUS_NEW = 'JOB_STATUS_NEW',
JOB_STATUS_RUNNING = 'JOB_STATUS_RUNNING',
JOB_STATUS_DONE = 'JOB_STATUS_DONE',
JOB_STATUS_SUSPENDED = 'JOB_STATUS_SUSPENDED',
}

/** gRPC-friendly result of a job. */
export enum JobResult {
JOB_RESULT_UNSPECIFIED = 'JOB_RESULT_UNSPECIFIED',
JOB_RESULT_PASS = 'JOB_RESULT_PASS',
JOB_RESULT_FAIL = 'JOB_RESULT_FAIL',
JOB_RESULT_ERROR = 'JOB_RESULT_ERROR',
JOB_RESULT_TIMEOUT = 'JOB_RESULT_TIMEOUT',
JOB_RESULT_ABORT = 'JOB_RESULT_ABORT',
JOB_RESULT_SKIP = 'JOB_RESULT_SKIP',
}

/** gRPC-friendly status of a session, decoupled from its result. */
export enum SessionStatus {
SESSION_STATUS_UNSPECIFIED = 'SESSION_STATUS_UNSPECIFIED',
SESSION_STATUS_NEW = 'SESSION_STATUS_NEW',
SESSION_STATUS_RUNNING = 'SESSION_STATUS_RUNNING',
SESSION_STATUS_DONE = 'SESSION_STATUS_DONE',
}

/** gRPC-friendly result of a session. */
export enum SessionResult {
SESSION_RESULT_UNSPECIFIED = 'SESSION_RESULT_UNSPECIFIED',
SESSION_RESULT_PASS = 'SESSION_RESULT_PASS',
SESSION_RESULT_FAIL = 'SESSION_RESULT_FAIL',
SESSION_RESULT_ERROR = 'SESSION_RESULT_ERROR',
SESSION_RESULT_TIMEOUT = 'SESSION_RESULT_TIMEOUT',
SESSION_RESULT_ABORT = 'SESSION_RESULT_ABORT',
SESSION_RESULT_SKIP = 'SESSION_RESULT_SKIP',
}

export {
JobResult,
JobStatus,
SessionResult,
SessionStatus,
TestResult,
TestStatus,
};
export type {ExecutionDetails, TimeBreakdown, Troubleshooting};
/** Minimal reference to the parent job. */
export interface ParentJobInfo {
id: string;
Expand All @@ -74,63 +33,24 @@ export interface ParentJobInfo {
result?: JobResult;
spongeLink?: string;
}

/** One device the test was executed on. */
export interface TestDevice {
id: string;
type: string;
}

/** Wrapper for a list of devices. */
export interface TestDevices {
device?: TestDevice[];
}

/** The host the test ran on. */
export interface HostInfo {
name: string;
ip: string;
}

/** Execution details. */
export interface ExecutionDetails {
user?: string;
actualUser?: string;
cloudLogLink?: string;
createTime: string;
startTime?: string;
endTime?: string;
lastUpdateTime?: string;
}

/** One non-fatal warning surfaced during execution. */
export interface TestWarning {
message: string;
trace?: string;
}

/** Wrapper for a list of warnings. */
export interface TestWarnings {
warning?: TestWarning[];
}

/** One error that contributed to a non-passing test result. */
export interface TestError {
message: string;
trace?: string;
}

/** Wrapper for a list of errors. */
export interface TestErrors {
error?: TestError[];
}

/** Troubleshooting details (warnings & errors). */
export interface Troubleshooting {
warnings?: TestWarnings;
resultCause?: TestErrors;
}

/** Alias for a warning info item belonging to a test. */
export type TestWarning = WarningInfo;
/** Alias for an error info item belonging to a test. */
export type TestError = ErrorInfo;
/** A lightweight summary of a test (for nested sub-tests). */
export interface TestSummary {
id: string;
Expand All @@ -141,18 +61,15 @@ export interface TestSummary {
endTime?: string;
devices?: TestDevices;
}

/** Wrapper for a list of test summaries. */
export interface TestSummaries {
test?: TestSummary[];
}

/** Nested sub-tests list details. */
export interface SubTestsInfo {
rootTestId?: string;
subTests?: TestSummaries;
}

/** The full overview data for a single test (matches TestDetail proto). */
export interface TestOverviewData {
id: string;
Expand All @@ -168,25 +85,23 @@ export interface TestOverviewData {
subTestsInfo?: SubTestsInfo;
timingBreakdown?: TimeBreakdown;
}

/** Request structure for the getTest API. */
export interface GetTestRequest {
testId: string;
subTestId?: string;
jobId: string;
}

/** Request structure for the getTestLog API. */
export interface GetTestLogRequest {
testId: string;
offset: number;
length: number;
jobId: string;
}

/** Response structure for the getTest API. */
export interface GetTestResponse {
test: TestOverviewData;
}

/** Response structure for the getTestLog API. */
export interface GetTestLogResponse {
logContent: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SCENARIO_TEST_ERROR: MockTestScenario = {
createTime: '2025-07-09T10:15:00Z',
startTime: '2025-07-09T10:15:05Z',
endTime: '2025-07-09T10:15:10Z',
lastUpdateTime: '2025-07-09T10:15:10Z',
updateTime: '2025-07-09T10:15:10Z',
user: 'dafeni',
actualUser: 'dafeni@google.com',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const SCENARIO_TEST_FAILED: MockTestScenario = {
createTime: '2025-07-09T10:12:10Z',
startTime: '2025-07-09T10:12:10Z',
endTime: '2025-07-09T10:12:40Z',
lastUpdateTime: '2025-07-09T10:12:40Z',
updateTime: '2025-07-09T10:12:40Z',
user: 'dafeni',
actualUser: 'dafeni@google.com',
},
Expand Down
Loading
Loading