Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"mocha": "nyc --reporter=html mocha 'src/**/*.spec.{ts,js}'",
"prepare": "npm run clean && npm run build:publish",
"test": "npm run lint:quiet && npm run test:compile && npm run mocha",
"test:appdistribution": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/appdistribution/*.spec.{ts,js}'",
"test:apptesting": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/apptesting/*.spec.{ts,js}'",
"test:client-integration": "bash ./scripts/client-integration-tests/run.sh",
"test:compile": "tsc --project tsconfig.compile.json",
Expand Down
89 changes: 88 additions & 1 deletion src/appdistribution/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as sinon from "sinon";
import * as tmp from "tmp";

import { AppDistributionClient } from "./client";
import { BatchRemoveTestersResponse, Group, TestDevice } from "./types";
import { BatchRemoveTestersResponse, Group, TestCase, TestDevice } from "./types";
import { appDistributionOrigin } from "../api";
import { Distribution } from "./distribution";
import { FirebaseError } from "../error";
Expand Down Expand Up @@ -501,4 +501,91 @@ describe("distribution", () => {
expect(nock.isDone()).to.be.true;
});
});

describe("listTestCases", () => {
it("should throw error if request fails", async () => {
nock(appDistributionOrigin())
.get(`/v1alpha/${appName}/testCases`)
.reply(400, { error: { status: "FAILED_PRECONDITION" } });
await expect(appDistributionClient.listTestCases(appName)).to.be.rejectedWith(
FirebaseError,
"Client failed to list test cases",
);
expect(nock.isDone()).to.be.true;
});

it("should resolve with array of test cases when request succeeds", async () => {
const testCases: TestCase[] = [
{
name: `$appName/testCases/tc_1`,
displayName: "Test Case 1",
aiInstructions: {
steps: [
{
goal: "Win at all costs",
},
],
},
},
{
name: `$appName/testCases/tc_2`,
displayName: "Test Case 2",
aiInstructions: { steps: [] },
},
];

nock(appDistributionOrigin()).get(`/v1alpha/${appName}/testCases`).reply(200, {
testCases: testCases,
});
await expect(appDistributionClient.listTestCases(appName)).to.eventually.deep.eq(testCases);
expect(nock.isDone()).to.be.true;
});
});

describe("createTestCase", () => {
const mockTestCase = { displayName: "Case", aiInstructions: { steps: [] } };

it("should throw error if request fails", async () => {
nock(appDistributionOrigin())
.post(`/v1alpha/${appName}/testCases`)
.reply(400, { error: { status: "FAILED_PRECONDITION" } });
await expect(appDistributionClient.createTestCase(appName, mockTestCase)).to.be.rejectedWith(
FirebaseError,
"Failed to create test case",
);
expect(nock.isDone()).to.be.true;
});

it("should resolve with TestCase when request succeeds", async () => {
nock(appDistributionOrigin()).post(`/v1alpha/${appName}/testCases`).reply(200, mockTestCase);
await expect(
appDistributionClient.createTestCase(appName, mockTestCase),
).to.be.eventually.deep.eq(mockTestCase);
expect(nock.isDone()).to.be.true;
});
});

describe("batchUpsertTestCases", () => {
const mockTestCase = { displayName: "Case", aiInstructions: { steps: [] } };

it("should throw error if request fails", async () => {
nock(appDistributionOrigin())
.post(`/v1alpha/${appName}/testCases:batchUpdate`)
.reply(400, { error: { status: "FAILED_PRECONDITION" } });
await expect(
appDistributionClient.batchUpsertTestCases(appName, [mockTestCase]),
).to.be.rejectedWith(FirebaseError, "Failed to upsert test cases");
expect(nock.isDone()).to.be.true;
});

it("should resolve with TestCase when request succeeds", async () => {
nock(appDistributionOrigin())
.post(`/v1alpha/${appName}/testCases:batchUpdate`)
.reply(200, { testCases: [mockTestCase] });
await expect(
appDistributionClient.batchUpsertTestCases(appName, [mockTestCase]),
).to.be.eventually.deep.eq([mockTestCase]);
expect(nock.isDone()).to.be.true;
});
});
});
65 changes: 61 additions & 4 deletions src/appdistribution/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
import {
AabInfo,
BatchRemoveTestersResponse,
BatchUpdateTestCasesRequest,
BatchUpdateTestCasesResponse,
Group,
ListGroupsResponse,
ListTestCasesResponse,
ListTestersResponse,
LoginCredential,
mapDeviceToExecution,
ReleaseTest,
TestCase,
TestDevice,
Tester,
UploadReleaseResponse,
Expand Down Expand Up @@ -111,15 +114,15 @@

try {
await this.appDistroV1Client.post(`/${releaseName}:distribute`, data);
} catch (err: any) {

Check warning on line 117 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
let errorMessage = err.message;

Check warning on line 118 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .message on an `any` value

Check warning on line 118 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
const errorStatus = err?.context?.body?.error?.status;

Check warning on line 119 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .context on an `any` value

Check warning on line 119 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (errorStatus === "FAILED_PRECONDITION") {
errorMessage = "invalid testers";
} else if (errorStatus === "INVALID_ARGUMENT") {
errorMessage = "invalid groups";
}
throw new FirebaseError(`failed to distribute to testers/groups: ${errorMessage}`, {

Check warning on line 125 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "any" of template literal expression
exit: 1,
});
}
Expand All @@ -144,8 +147,8 @@
apiResponse = await client.get<ListTestersResponse>(`${projectName}/testers`, {
queryParams,
});
} catch (err) {
} catch (err: unknown) {
throw new FirebaseError(`Client request failed to list testers ${err}`);

Check warning on line 151 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
}

for (const t of apiResponse.body.testers ?? []) {
Expand Down Expand Up @@ -206,8 +209,8 @@
});
groups.push(...(apiResponse.body.groups ?? []));
pageToken = apiResponse.body.nextPageToken;
} catch (err) {
} catch (err: unknown) {
throw new FirebaseError(`Client failed to list groups ${err}`);

Check warning on line 213 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
}
} while (pageToken);
return groups;
Expand Down Expand Up @@ -280,7 +283,7 @@
method: "POST",
path: `${releaseName}/tests`,
body: {
deviceExecutions: devices.map(mapDeviceToExecution),
deviceExecutions: devices.map((device) => ({ device })),
loginCredential,
testCase: testCaseName,
},
Expand All @@ -295,4 +298,58 @@
const response = await this.appDistroV1AlphaClient.get<ReleaseTest>(releaseTestName);
return response.body;
}

async listTestCases(appName: string): Promise<TestCase[]> {
const testCases: TestCase[] = [];
const client = this.appDistroV1AlphaClient;

let pageToken: string | undefined;
do {
const queryParams: Record<string, string> = pageToken ? { pageToken } : {};
try {
const apiResponse = await client.get<ListTestCasesResponse>(`${appName}/testCases`, {
queryParams,
});
testCases.push(...(apiResponse.body.testCases ?? []));
pageToken = apiResponse.body.nextPageToken;
} catch (err: unknown) {
throw new FirebaseError(`Client failed to list test cases ${err}`);

Check warning on line 316 in src/appdistribution/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
}
} while (pageToken);
return testCases;
}

async createTestCase(appName: string, testCase: TestCase): Promise<TestCase> {
try {
const response = await this.appDistroV1AlphaClient.request<TestCase, TestCase>({
method: "POST",
path: `${appName}/testCases`,
body: testCase,
});
return response.body;
} catch (err: unknown) {
throw new FirebaseError(`Failed to create test case ${getErrMsg(err)}`);
}
}

async batchUpsertTestCases(appName: string, testCases: TestCase[]): Promise<TestCase[]> {
try {
const response = await this.appDistroV1AlphaClient.request<
BatchUpdateTestCasesRequest,
BatchUpdateTestCasesResponse
>({
method: "POST",
path: `${appName}/testCases:batchUpdate`,
body: {
requests: testCases.map((tc) => ({
testCase: tc,
allowMissing: true,
})),
},
});
return response.body.testCases;
} catch (err: unknown) {
throw new FirebaseError(`Failed to upsert test cases ${getErrMsg(err)}`);
}
}
}
46 changes: 35 additions & 11 deletions src/appdistribution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,6 @@ export interface DeviceExecution {
inconclusiveReason?: string;
}

export function mapDeviceToExecution(device: TestDevice): DeviceExecution {
return {
device: {
model: device.model,
version: device.version,
orientation: device.orientation,
locale: device.locale,
},
};
}

export interface FieldHints {
usernameResourceName?: string;
passwordResourceName?: string;
Expand All @@ -128,3 +117,38 @@ export interface ReleaseTest {
loginCredential?: LoginCredential;
testCase?: string;
}

export interface AiStep {
goal: string;
hint?: string;
successCriteria?: string;
}

export interface AiInstructions {
steps: AiStep[];
}

export interface TestCase {
name?: string;
displayName: string;
prerequisiteTestCase?: string;
aiInstructions: AiInstructions;
}

export interface ListTestCasesResponse {
testCases: TestCase[];
nextPageToken?: string;
}

export interface UpdateTestCaseRequest {
testCase: TestCase;
allowMissing?: boolean;
}

export interface BatchUpdateTestCasesRequest {
requests: UpdateTestCaseRequest[];
}

export interface BatchUpdateTestCasesResponse {
testCases: TestCase[];
}
Loading
Loading