Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added `functions.list_functions` as a MCP tool (#9369)
- Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185)
- Improved error messages for Firebase AI Logic provisioning during 'firebase init' (#9377)
- Added `appdistribution:testcases:export` and `appdistribution:testcases:import` (#9397)
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;
});
});
});
71 changes: 64 additions & 7 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,9 +114,9 @@

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;
let errorMessage = getErrMsg(err);
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") {
Expand Down Expand Up @@ -144,8 +147,8 @@
apiResponse = await client.get<ListTestersResponse>(`${projectName}/testers`, {
queryParams,
});
} catch (err) {
throw new FirebaseError(`Client request failed to list testers ${err}`);
} catch (err: unknown) {
throw new FirebaseError(`Client request failed to list testers ${getErrMsg(err)}`);
}

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) {
throw new FirebaseError(`Client failed to list groups ${err}`);
} catch (err: unknown) {
throw new FirebaseError(`Client failed to list groups ${getErrMsg(err)}`);
}
} 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 ${getErrMsg(err)}`);
}
} 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