Skip to content

Commit b9bb453

Browse files
authored
Adding Angular functional test (#232)
* Add Angular e2e test * Fix minor issue with cache-control headers for SSG content
1 parent a4c33c9 commit b9bb453

File tree

11 files changed

+3787
-4698
lines changed

11 files changed

+3787
-4698
lines changed
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
/e2e
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
runs
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import assert from "assert";
2+
import { posix } from "path";
3+
4+
export const host = process.env.HOST;
5+
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
const isLocalhost = new URL(host).hostname === "localhost";
11+
12+
describe("common", () => {
13+
it("/", async () => {
14+
const response = await fetch(host);
15+
assert.ok(response.ok);
16+
assert.equal(response.headers.get("content-type")?.toLowerCase(), "text/html; charset=utf-8");
17+
assert.equal(response.headers.get("cache-control"), null);
18+
});
19+
20+
it("/favicon.ico", async () => {
21+
const response = await fetch(posix.join(host, "favicon.ico"));
22+
assert.ok(response.ok);
23+
assert.equal(response.headers.get("content-type"), "image/x-icon");
24+
assert.equal(
25+
response.headers.get("cache-control"),
26+
isLocalhost ? "public, max-age=31536000" : "public,max-age=60",
27+
);
28+
});
29+
30+
it(`404`, async () => {
31+
const response = await fetch(posix.join(host, Math.random().toString()));
32+
assert.ok(response.ok);
33+
assert.equal(response.headers.get("content-type")?.toLowerCase(), "text/html; charset=utf-8");
34+
assert.equal(response.headers.get("cache-control"), null);
35+
});
36+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { cp, readFile, writeFile } from "fs/promises";
2+
import promiseSpawn from "@npmcli/promise-spawn";
3+
import { dirname, join, relative } from "path";
4+
import { fileURLToPath } from "url";
5+
import { parse as parseYaml } from "yaml";
6+
import { spawn } from "child_process";
7+
import fsExtra from "fs-extra";
8+
9+
const { readFileSync, mkdirp, readJSON, writeJSON, rmdir } = fsExtra;
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
12+
const starterTemplateDir = "../../../starters/angular/basic";
13+
14+
const errors: any[] = [];
15+
16+
await rmdir(join(__dirname, "runs"), { recursive: true }).catch(() => undefined);
17+
18+
console.log("\nBuilding and starting test projects in parallel...");
19+
20+
const tests = await Promise.all(
21+
[
22+
[false, false],
23+
[false, true],
24+
[true, false],
25+
[true, true],
26+
].map(async ([enableSSR, enableSSG]) => {
27+
const runId = Math.random().toString().split(".")[1];
28+
const cwd = join(__dirname, "runs", runId);
29+
await mkdirp(cwd);
30+
31+
console.log(`[${runId}] Copying ${starterTemplateDir} to working directory`);
32+
await cp(starterTemplateDir, cwd, { recursive: true });
33+
34+
const packageJSON = await readJSON(join(cwd, "package.json"));
35+
packageJSON.name = `firebase-app-hosting-angular-${runId}`;
36+
await writeJSON(join(cwd, "package.json"), packageJSON);
37+
38+
console.log(`[${runId}] > npm ci --silent --no-progress`);
39+
await promiseSpawn("npm", ["ci", "--silent", "--no-progress"], {
40+
cwd,
41+
stdio: "inherit",
42+
shell: true,
43+
});
44+
45+
const angularJSON = JSON.parse((await readFile(join(cwd, "angular.json"))).toString());
46+
47+
if (!enableSSR) {
48+
console.log(`[${runId}] Disabling SSR option in angular.json`);
49+
angularJSON.projects["firebase-app-hosting-angular"].architect.build.options.ssr = false;
50+
}
51+
if (!enableSSG) {
52+
console.log(`[${runId}] Disabling prerender option in angular.json`);
53+
angularJSON.projects["firebase-app-hosting-angular"].architect.build.options.prerender =
54+
false;
55+
}
56+
await writeFile(join(cwd, "angular.json"), JSON.stringify(angularJSON, null, 2));
57+
58+
const buildScript = relative(cwd, join(__dirname, "../dist/bin/build.js"));
59+
console.log(`[${runId}] > node ${buildScript}`);
60+
61+
const frameworkVersion = JSON.parse(
62+
readFileSync(join(cwd, "node_modules", "@angular", "core", "package.json"), "utf-8"),
63+
).version;
64+
await promiseSpawn("node", [buildScript], {
65+
cwd,
66+
stdio: "inherit",
67+
shell: true,
68+
env: {
69+
...process.env,
70+
FRAMEWORK_VERSION: frameworkVersion,
71+
},
72+
});
73+
74+
const bundleYaml = parseYaml(readFileSync(join(cwd, ".apphosting/bundle.yaml")).toString());
75+
76+
const runCommand = bundleYaml.runCommand;
77+
78+
if (typeof runCommand !== "string") {
79+
throw new Error("runCommand must be a string");
80+
}
81+
82+
const [runScript, ...runArgs] = runCommand.split(" ");
83+
let resolveHostname: (it: string) => void;
84+
let rejectHostname: () => void;
85+
const hostnamePromise = new Promise<string>((resolve, reject) => {
86+
resolveHostname = resolve;
87+
rejectHostname = reject;
88+
});
89+
const port = 8080 + Math.floor(Math.random() * 1000);
90+
console.log(`[${runId}] > PORT=${port} ${runCommand}`);
91+
const run = spawn(runScript, runArgs, {
92+
cwd,
93+
shell: true,
94+
env: {
95+
...process.env,
96+
NODE_ENV: "production",
97+
PORT: port.toString(),
98+
},
99+
});
100+
run.stderr.on("data", (data) => console.error(data.toString()));
101+
run.stdout.on("data", (data) => {
102+
console.log(data.toString());
103+
if (data.toString() === `Node Express server listening on http://localhost:${port}\n`) {
104+
resolveHostname(`http://localhost:${port}`);
105+
} else {
106+
run.stdin.end();
107+
run.kill("SIGKILL");
108+
}
109+
});
110+
run.on("close", (code) => {
111+
if (code) {
112+
rejectHostname();
113+
}
114+
});
115+
const host = await hostnamePromise;
116+
117+
return [host, run, enableSSR, enableSSG] as const;
118+
}),
119+
);
120+
121+
console.log("\n\n");
122+
123+
for (const [host, run, enableSSR, enableSSG] of tests) {
124+
try {
125+
console.log(
126+
`> HOST=${host}${enableSSR ? " SSR=1" : ""}${
127+
enableSSG ? " SSG=1" : ""
128+
} ts-mocha -p tsconfig.json e2e/*.spec.ts`,
129+
);
130+
await promiseSpawn("ts-mocha", ["-p", "tsconfig.json", "e2e/*.spec.ts"], {
131+
shell: true,
132+
stdio: "inherit",
133+
env: {
134+
...process.env,
135+
SSR: enableSSR ? "1" : undefined,
136+
SSG: enableSSG ? "1" : undefined,
137+
HOST: host,
138+
},
139+
}).finally(() => {
140+
run.stdin.end();
141+
run.kill("SIGKILL");
142+
});
143+
} catch (e) {
144+
errors.push(e);
145+
}
146+
}
147+
148+
if (errors.length) {
149+
console.error(errors);
150+
process.exit(1);
151+
}
152+
153+
process.exit(0);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import assert from "assert";
2+
import { posix } from "path";
3+
4+
const host = process.env.HOST;
5+
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
describe("single-page app", () => {
11+
before(function () {
12+
if (process.env.SSR || process.env.SSG) {
13+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
14+
this.skip();
15+
}
16+
});
17+
18+
// eslint-disable-next-line prefer-arrow-callback
19+
it("/", async () => {
20+
const response = await fetch(host);
21+
const body = await response.text();
22+
assert(body.includes("<app-root></app-root>"));
23+
});
24+
25+
it(`404`, async () => {
26+
const response = await fetch(posix.join(host, Math.random().toString()));
27+
const body = await response.text();
28+
assert(body.includes("<app-root></app-root>"));
29+
});
30+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import assert from "assert";
2+
import { posix } from "path";
3+
4+
const host = process.env.HOST;
5+
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
describe("static-site generation", () => {
11+
before(function () {
12+
if (!process.env.SSG && !process.env.SSR) {
13+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
14+
this.skip();
15+
}
16+
});
17+
18+
it("/", async () => {
19+
const response = await fetch(host);
20+
const body = await response.text();
21+
assert(body.includes("Angular on Firebase App Hosting</h1>"));
22+
});
23+
24+
it("/ssg", async () => {
25+
const response = await fetch(posix.join(host, "ssg"));
26+
assert.ok(response.ok);
27+
assert.equal(response.headers.get("content-type")?.toLowerCase(), "text/html; charset=utf-8");
28+
assert.equal(response.headers.get("cache-control"), null);
29+
const body = await response.text();
30+
assert(body.includes("SSG</h1>"));
31+
});
32+
33+
it("/deferrable-views", async () => {
34+
const response = await fetch(posix.join(host, "deferrable-views"));
35+
assert.ok(response.ok);
36+
assert.equal(response.headers.get("content-type")?.toLowerCase(), "text/html; charset=utf-8");
37+
assert.equal(response.headers.get("cache-control"), null);
38+
const body = await response.text();
39+
assert(body.includes("Deferrable Views</h1>"));
40+
}).timeout(10_000);
41+
42+
it(`404`, async function () {
43+
if (process.env.SSR) {
44+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
45+
return this.skip();
46+
}
47+
const response = await fetch(posix.join(host, Math.random().toString()));
48+
const body = await response.text();
49+
assert(body.includes("Angular on Firebase App Hosting</h1>"));
50+
});
51+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import assert from "assert";
2+
import { posix } from "path";
3+
4+
const host = process.env.HOST;
5+
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
describe("server-side rendering", () => {
11+
before(function () {
12+
if (!process.env.SSR) {
13+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
14+
this.skip();
15+
}
16+
});
17+
18+
it("/ssr", async () => {
19+
const response = await fetch(posix.join(host, "ssr"));
20+
assert.ok(response.ok);
21+
assert.equal(response.headers.get("content-type"), "text/html; charset=utf-8");
22+
assert.equal(response.headers.get("cache-control"), null);
23+
});
24+
25+
it(`404`, async () => {
26+
const response = await fetch(posix.join(host, Math.random().toString()));
27+
const body = await response.text();
28+
assert(body.includes("Page not found</h1>"));
29+
});
30+
});

packages/@apphosting/adapter-angular/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
"sideEffects": false,
2323
"scripts": {
2424
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
25-
"test": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
25+
"test": "npm run test:unit && npm run test:functional",
26+
"test:unit": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
27+
"test:functional": "node --loader ts-node/esm ./e2e/run-local.ts",
2628
"localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml",
2729
"localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/adapter-angular && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873"
2830
},

0 commit comments

Comments
 (0)