Skip to content
Merged
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
172 changes: 172 additions & 0 deletions src/apptesting/parseTestFiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
rmSync(tempdir.name, { recursive: true });
});

function writeFile(filename: string, content: string) {

Check warning on line 23 in src/apptesting/parseTestFiles.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const file = join(tempdir.name, filename);
fs.writeFileSync(file, content);
}
Expand Down Expand Up @@ -53,6 +53,8 @@
expect(tests).to.eql([
{
testCase: {
id: undefined,
prerequisiteTestCaseId: undefined,
displayName: "my test",
startUri: "http://www.foo.com/mypage",
instructions: {
Expand All @@ -74,6 +76,8 @@
expect(tests).to.eql([
{
testCase: {
id: undefined,
prerequisiteTestCaseId: undefined,
displayName: "Smoke test",
startUri: "http://www.foo.com",
instructions: {
Expand Down Expand Up @@ -115,6 +119,8 @@
expect(tests).to.eql([
{
testCase: {
id: undefined,
prerequisiteTestCaseId: undefined,
displayName: "my test",
startUri: "https://www.foo.com",
instructions: {
Expand All @@ -129,6 +135,8 @@
},
{
testCase: {
id: undefined,
prerequisiteTestCaseId: undefined,
displayName: "my second test",
startUri: "https://www.foo.com",
instructions: {
Expand All @@ -144,6 +152,8 @@

{
testCase: {
id: undefined,
prerequisiteTestCaseId: undefined,
displayName: "my third test",
startUri: "https://www.foo.com/mypage",
instructions: {
Expand All @@ -161,7 +171,7 @@
});

describe("filtering", () => {
function createBasicTest(testNames: string[]) {

Check warning on line 174 in src/apptesting/parseTestFiles.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
return stringify({
tests: testNames.map((testName) => ({
testName,
Expand All @@ -170,7 +180,7 @@
});
}

async function getTestCaseNames(filenameFilter = "", testCaseFilter = "") {

Check warning on line 183 in src/apptesting/parseTestFiles.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const tests = await parseTestFiles(
tempdir.name,
"https://www.foo.com",
Expand Down Expand Up @@ -204,4 +214,166 @@
expect(await getTestCaseNames("a$", "xx")).to.eql(["axx"]);
});
});
describe("prerequisite test cases", () => {
it("merges the steps from the prerequisite test case", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
id: "my-first-test",
testName: "my first test",
steps: [{ goal: "do something first" }],
},
{
testName: "my second test",
prerequisiteTestCaseId: "my-first-test",
steps: [{ goal: "do something second" }],
},
],
}),
);

const tests = await parseTestFiles(tempdir.name, "https://www.foo.com");
expect(tests.length).to.equal(2);
const secondTest = tests[1];
expect(secondTest.testCase.instructions.steps).to.eql([
{ goal: "do something first" },
{ goal: "do something second" },
]);
});

it("throws an error for a non-existent prerequisite test case", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
testName: "my second test",
prerequisiteTestCaseId: "my-first-test",
steps: [{ goal: "do something second" }],
},
],
}),
);

await expect(parseTestFiles(tempdir.name, "https://www.foo.com")).to.be.rejectedWith(
FirebaseError,
"Invalid prerequisiteTestCaseId. There is no test case with id my-first-test",
);
});

it("handles an undefined prerequisite test case id", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
testName: "my test",
steps: [{ goal: "do something" }],
},
],
}),
);

const tests = await parseTestFiles(tempdir.name, "https://www.foo.com");
expect(tests.length).to.equal(1);
expect(tests[0].testCase.instructions.steps).to.eql([{ goal: "do something" }]);
});

it("works correctly with filtering", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
id: "my-first-test",
testName: "my first test",
steps: [{ goal: "do something first" }],
},
{
testName: "my second test",
prerequisiteTestCaseId: "my-first-test",
steps: [{ goal: "do something second" }],
},
],
}),
);

const tests = await parseTestFiles(
tempdir.name,
"https://www.foo.com",
/* filePattern= */ "",
/* namePattern= */ "my second test",
);
expect(tests.length).to.equal(1);
const secondTest = tests[0];
expect(secondTest.testCase.instructions.steps).to.eql([
{ goal: "do something first" },
{ goal: "do something second" },
]);
});

it("works correctly with multiple levels of prerequisites", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
id: "my-first-test",
testName: "my first test",
steps: [{ goal: "do something first" }],
},
{
id: "my-second-test",
testName: "my second test",
prerequisiteTestCaseId: "my-first-test",
steps: [{ goal: "do something second" }],
},
{
testName: "my third test",
prerequisiteTestCaseId: "my-second-test",
steps: [{ goal: "do something third" }],
},
],
}),
);

const tests = await parseTestFiles(tempdir.name, "https://www.foo.com");
expect(tests.length).to.equal(3);
const thirdTest = tests[2];
expect(thirdTest.testCase.instructions.steps).to.eql([
{ goal: "do something first" },
{ goal: "do something second" },
{ goal: "do something third" },
]);
});

it("throws error if there is a circular depedency", async () => {
writeFile(
"my_test.yaml",
stringify({
tests: [
{
id: "my-first-test",
testName: "my first test",
prerequisiteTestCaseId: "my-second-test",
steps: [{ goal: "do something first" }],
},
{
id: "my-second-test",
testName: "my second test",
prerequisiteTestCaseId: "my-first-test",
steps: [{ goal: "do something second" }],
},
],
}),
);

await expect(parseTestFiles(tempdir.name, "https://www.foo.com")).to.be.rejectedWith(
FirebaseError,
"Detected a cycle in prerequisite test cases.",
);
});
});
});
142 changes: 107 additions & 35 deletions src/apptesting/parseTestFiles.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { dirExistsSync, fileExistsSync, listFiles } from "../fsutils";
import { join } from "path";
import { logger } from "../logger";
import { Browser, TestCaseInvocation } from "./types";
import { Browser, TestCaseInvocation, TestStep } from "./types";
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
import { FirebaseError, getErrMsg, getError } from "../error";

function createFilter(pattern?: string) {
const regex = pattern ? new RegExp(pattern) : undefined;
return (s: string) => !regex || regex.test(s);
}

export async function parseTestFiles(

Check warning on line 8 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
dir: string,
targetUri?: string,
filePattern?: string,
Expand All @@ -26,52 +21,129 @@
}
}

const files = await parseTestFilesRecursive({ testDir: dir, targetUri });
const idToInvocation = files
.flatMap((file) => file.invocations)
.reduce(
(accumulator, invocation) => {
if (invocation.testCase.id) {
accumulator[invocation.testCase.id] = invocation;
}
return accumulator;
},
{} as Record<string, TestCaseInvocation>,
);

const fileFilterFn = createFilter(filePattern);
const nameFilterFn = createFilter(namePattern);
const filteredInvocations = files
.filter((file) => fileFilterFn(file.path))
.flatMap((file) => file.invocations)
.filter((invocation) => nameFilterFn(invocation.testCase.displayName));

return filteredInvocations.map((invocation) => {
let prerequisiteTestCaseId = invocation.testCase.prerequisiteTestCaseId;
if (prerequisiteTestCaseId === undefined) {
return invocation;
}

async function parseTestFilesRecursive(testDir: string): Promise<TestCaseInvocation[]> {
const items = listFiles(testDir);
const results = [];
for (const item of items) {
const path = join(testDir, item);
if (dirExistsSync(path)) {
results.push(...(await parseTestFilesRecursive(path)));
} else if (fileFilterFn(path) && fileExistsSync(path)) {
try {
const file = await readFileFromDirectory(testDir, item);
const parsedFile = wrappedSafeLoad(file.source);
const tests = parsedFile.tests;
const defaultConfig = parsedFile.defaultConfig;
if (!tests || !tests.length) {
logger.info(`No tests found in ${path}. Ignoring.`);
continue;
}
for (const rawTestDef of parsedFile.tests) {
if (!nameFilterFn(rawTestDef.testName)) continue;
const testDef = toTestDef(rawTestDef, defaultConfig, targetUri);
results.push(testDef);
}
} catch (ex) {
const errMsg = getErrMsg(ex);
const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
const prerequisiteSteps: TestStep[] = [];
const previousTestCaseIds = new Set<string>();
while (prerequisiteTestCaseId) {
if (previousTestCaseIds.has(prerequisiteTestCaseId)) {
throw new FirebaseError(`Detected a cycle in prerequisite test cases.`);
}
previousTestCaseIds.add(prerequisiteTestCaseId);
const prerequisiteTestCaseInvocation: TestCaseInvocation | undefined =
idToInvocation[prerequisiteTestCaseId];
if (prerequisiteTestCaseInvocation === undefined) {
throw new FirebaseError(
`Invalid prerequisiteTestCaseId. There is no test case with id ${prerequisiteTestCaseId}`,
);
}
prerequisiteSteps.unshift(...prerequisiteTestCaseInvocation.testCase.instructions.steps);
prerequisiteTestCaseId = prerequisiteTestCaseInvocation.testCase.prerequisiteTestCaseId;
}

return {
...invocation,
testCase: {
...invocation.testCase,
instructions: {
...invocation.testCase.instructions,
steps: prerequisiteSteps.concat(invocation.testCase.instructions.steps),
},
},
};
});
}

function createFilter(pattern?: string) {

Check warning on line 81 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const regex = pattern ? new RegExp(pattern) : undefined;
return (s: string) => !regex || regex.test(s);
}

interface TestCaseFile {
path: string;
invocations: TestCaseInvocation[];
}

async function parseTestFilesRecursive(params: {
testDir: string;
targetUri?: string;
}): Promise<TestCaseFile[]> {
const testDir = params.testDir;
const targetUri = params.targetUri;
const items = listFiles(testDir);
const results = [];
for (const item of items) {
const path = join(testDir, item);
if (dirExistsSync(path)) {
results.push(...(await parseTestFilesRecursive({ testDir: path, targetUri })));
} else if (fileExistsSync(path)) {
try {
const file = await readFileFromDirectory(testDir, item);
logger.debug(`Read the file ${file.source}.`);
const parsedFile = wrappedSafeLoad(file.source);

Check warning on line 107 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
logger.debug(`Parsed the file.`);
const tests = parsedFile.tests;

Check warning on line 109 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .tests on an `any` value

Check warning on line 109 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
logger.debug(`There are ${tests.length} tests.`);

Check warning on line 110 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .length on an `any` value

Check warning on line 110 in src/apptesting/parseTestFiles.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "any" of template literal expression
const defaultConfig = parsedFile.defaultConfig;
if (!tests || !tests.length) {
logger.debug(`No tests found in ${path}. Ignoring.`);
continue;
}
const invocations = [];
for (const rawTestDef of tests) {
const invocation = toTestCaseInvocation(rawTestDef, targetUri, defaultConfig);
invocations.push(invocation);
}
results.push({ path, invocations: invocations });
} catch (ex) {
const errMsg = getErrMsg(ex);
const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
logger.debug(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
continue;
}
}
return results;
}

return parseTestFilesRecursive(dir);
return results;
}

function toTestDef(testDef: any, defaultConfig: any, targetUri?: string): TestCaseInvocation {
function toTestCaseInvocation(
testDef: any,
targetUri: any,
defaultConfig: any,
): TestCaseInvocation {
const steps = testDef.steps ?? [];
const route = testDef.testConfig?.route ?? defaultConfig?.route ?? "";
const browsers: Browser[] = testDef.testConfig?.browsers ??
defaultConfig?.browsers ?? [Browser.CHROME];
return {
testCase: {
id: testDef.id,
prerequisiteTestCaseId: testDef.prerequisiteTestCaseId,
startUri: targetUri + route,
displayName: testDef.testName,
instructions: { steps },
Expand Down
2 changes: 2 additions & 0 deletions src/apptesting/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ export interface TestExecutionResult {
}

export interface TestCase {
id?: string;
startUri?: string;
displayName: string;
instructions: Instructions;
prerequisiteTestCaseId?: string;
}

export interface Instructions {
Expand Down
Loading