Skip to content

Commit 397abf5

Browse files
committed
test: migrate build and misc E2E tests to Puppeteer
Replaces the Protractor-based `ng e2e` execution with the new Puppeteer `executeBrowserTest` utility in various build and misc E2E tests. The following tests were updated: - express-engine-csp-nonce - express-engine-ngmodule - express-engine-standalone - app-shell-with-service-worker - auto-csp - worker - trusted-types This migration involves implementing custom `checkFn` logic to replicate the assertions previously handled by Protractor, such as verifying server-rendered content, console logs, and service worker states.
1 parent 95e35e5 commit 397abf5

File tree

7 files changed

+232
-344
lines changed

7 files changed

+232
-344
lines changed

tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { appendToFile, expectFileToMatch, writeFile } from '../../../utils/fs';
33
import { installPackage } from '../../../utils/packages';
44
import { ng } from '../../../utils/process';
55
import { updateJsonFile } from '../../../utils/project';
6+
import { executeBrowserTest } from '../../../utils/puppeteer';
67

78
const snapshots = require('../../../ng-snapshot/package.json');
89

@@ -31,26 +32,26 @@ export default async function () {
3132
}
3233
}
3334

34-
await writeFile(
35-
'e2e/app.e2e-spec.ts',
36-
`
37-
import { browser, by, element } from 'protractor';
35+
await ng('build');
36+
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
3837

39-
it('should have ngsw in normal state', () => {
40-
browser.get('/');
38+
await executeBrowserTest({
39+
configuration: 'production',
40+
checkFn: async (page) => {
4141
// Wait for service worker to load.
42-
browser.sleep(2000);
43-
browser.waitForAngularEnabled(false);
44-
browser.get('/ngsw/state');
45-
// Should have updated, and be in normal state.
46-
expect(element(by.css('pre')).getText()).not.toContain('Last update check: never');
47-
expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL');
48-
});
49-
`,
50-
);
42+
await new Promise((resolve) => setTimeout(resolve, 2000));
5143

52-
await ng('build');
53-
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
44+
const baseUrl = page.url();
45+
await page.goto(new URL('/ngsw/state', baseUrl).href);
5446

55-
await ng('e2e', '--configuration=production');
47+
// Should have updated, and be in normal state.
48+
const preText = await page.$eval('pre', (el) => el.textContent);
49+
if (preText?.includes('Last update check: never')) {
50+
throw new Error(`Expected service worker to have checked for updates, but got: ${preText}`);
51+
}
52+
if (!preText?.includes('Driver state: NORMAL')) {
53+
throw new Error(`Expected service worker driver state to be NORMAL, but got: ${preText}`);
54+
}
55+
},
56+
});
5657
}

tests/e2e/tests/build/auto-csp.ts

Lines changed: 46 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import assert from 'node:assert';
2+
import { setTimeout } from 'node:timers/promises';
23
import { getGlobalVariable } from '../../utils/env';
34
import { expectFileToMatch, writeFile, writeMultipleFiles } from '../../utils/fs';
45
import { findFreePort } from '../../utils/network';
56
import { execAndWaitForOutputToMatch, ng } from '../../utils/process';
67
import { updateJsonFile } from '../../utils/project';
8+
import { executeBrowserTest } from '../../utils/puppeteer';
79

810
const CSP_META_TAG = /<meta http-equiv="Content-Security-Policy"/;
911

@@ -67,55 +69,6 @@ export default async function () {
6769
</body>
6870
</html>
6971
`,
70-
'e2e/src/app.e2e-spec.ts': `
71-
import { browser, by, element } from 'protractor';
72-
import * as webdriver from 'selenium-webdriver';
73-
74-
function allConsoleWarnMessagesAndErrors() {
75-
return browser
76-
.manage()
77-
.logs()
78-
.get('browser')
79-
.then(function (browserLog: any[]) {
80-
const warnMessages: any[] = [];
81-
browserLog.filter((logEntry) => {
82-
const msg = logEntry.message;
83-
console.log('>> ' + msg);
84-
if (logEntry.level.value >= webdriver.logging.Level.INFO.value) {
85-
warnMessages.push(msg);
86-
}
87-
});
88-
return warnMessages;
89-
});
90-
}
91-
92-
describe('Hello world E2E Tests', () => {
93-
beforeAll(async () => {
94-
await browser.waitForAngularEnabled(true);
95-
});
96-
97-
it('should display: Welcome and run all scripts in order', async () => {
98-
// Load the page without waiting for Angular since it is not bootstrapped automatically.
99-
await browser.driver.get(browser.baseUrl);
100-
101-
// Test the contents.
102-
expect(await element(by.css('h1')).getText()).toMatch('Hello');
103-
104-
// Make sure all scripts ran and there were no client side errors.
105-
const consoleMessages = await allConsoleWarnMessagesAndErrors();
106-
expect(consoleMessages.length).toEqual(4); // No additional errors
107-
// Extract just the printed messages from the console data.
108-
const printedMessages = consoleMessages.map(m => m.match(/"(.*?)"/)[1]);
109-
expect(printedMessages).toEqual([
110-
// All messages printed in order because execution order is preserved.
111-
"Inline Script Head",
112-
"Inline Script Body: 1339",
113-
"First External Script: 1338",
114-
"Second External Script: 1337",
115-
]);
116-
});
117-
});
118-
`,
11972
});
12073

12174
async function spawnServer(): Promise<number> {
@@ -137,7 +90,49 @@ export default async function () {
13790
// Make sure if contains the critical CSS inlining CSP code.
13891
await expectFileToMatch('dist/test-project/browser/index.html', 'ngCspMedia');
13992

140-
// Make sure that our e2e protractor tests run to confirm that our angular project runs.
93+
// Make sure that our e2e tests run to confirm that our angular project runs.
14194
const port = await spawnServer();
142-
await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target=');
95+
await executeBrowserTest({
96+
baseUrl: `http://localhost:${port}/`,
97+
checkFn: async (page) => {
98+
const warnMessages: string[] = [];
99+
page.on('console', (msg) => {
100+
if (msg.type() === 'warning') {
101+
warnMessages.push(msg.text());
102+
}
103+
});
104+
105+
// Reload to ensure we capture messages from the start if needed,
106+
// although executeBrowserTest already navigated.
107+
await page.reload();
108+
109+
// Wait for the expected number of warnings
110+
let retries = 50;
111+
while (warnMessages.length < 4 && retries > 0) {
112+
await setTimeout(100);
113+
retries--;
114+
}
115+
116+
if (warnMessages.length !== 4) {
117+
throw new Error(
118+
`Expected 4 console warnings, but got ${warnMessages.length}:\n${warnMessages.join('\n')}`,
119+
);
120+
}
121+
122+
const expectedMessages = [
123+
'Inline Script Head',
124+
'Inline Script Body: 1339',
125+
'First External Script: 1338',
126+
'Second External Script: 1337',
127+
];
128+
129+
for (let i = 0; i < expectedMessages.length; i++) {
130+
if (!warnMessages[i].includes(expectedMessages[i])) {
131+
throw new Error(
132+
`Expected warning ${i} to include '${expectedMessages[i]}', but got '${warnMessages[i]}'`,
133+
);
134+
}
135+
}
136+
},
137+
});
143138
}

tests/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts

Lines changed: 48 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { findFreePort } from '../../../utils/network';
44
import { installWorkspacePackages } from '../../../utils/packages';
55
import { execAndWaitForOutputToMatch, ng } from '../../../utils/process';
66
import { updateJsonFile, updateServerFileForEsbuild, useSha } from '../../../utils/project';
7+
import { executeBrowserTest } from '../../../utils/puppeteer';
78

89
export default async function () {
910
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
@@ -46,90 +47,6 @@ export default async function () {
4647
</body>
4748
</html>
4849
`,
49-
'e2e/src/app.e2e-spec.ts': `
50-
import { browser, by, element } from 'protractor';
51-
import * as webdriver from 'selenium-webdriver';
52-
53-
function verifyNoBrowserErrors() {
54-
return browser
55-
.manage()
56-
.logs()
57-
.get('browser')
58-
.then(function (browserLog: any[]) {
59-
const errors: any[] = [];
60-
browserLog.filter((logEntry) => {
61-
const msg = logEntry.message;
62-
console.log('>> ' + msg);
63-
if (logEntry.level.value >= webdriver.logging.Level.INFO.value) {
64-
errors.push(msg);
65-
}
66-
});
67-
expect(errors).toEqual([]);
68-
});
69-
}
70-
71-
describe('Hello world E2E Tests', () => {
72-
beforeAll(async () => {
73-
await browser.waitForAngularEnabled(false);
74-
});
75-
76-
it('should display: Welcome', async () => {
77-
// Load the page without waiting for Angular since it is not bootstrapped automatically.
78-
await browser.driver.get(browser.baseUrl);
79-
80-
expect(
81-
await element(by.css('style[ng-app-id="ng"]')).getText()
82-
).not.toBeNull();
83-
84-
// Test the contents from the server.
85-
expect(await element(by.css('h1')).getText()).toMatch('Hello');
86-
87-
// Bootstrap the client side app.
88-
await browser.executeScript('doBootstrap()');
89-
90-
// Retest the contents after the client bootstraps.
91-
expect(await element(by.css('h1')).getText()).toMatch('Hello');
92-
93-
// Make sure the server styles got replaced by client side ones.
94-
expect(
95-
await element(by.css('style[ng-app-id="ng"]')).isPresent()
96-
).toBeFalsy();
97-
expect(await element(by.css('style')).getText()).toMatch('');
98-
99-
// Make sure there were no client side errors.
100-
await verifyNoBrowserErrors();
101-
});
102-
103-
it('stylesheets should be configured to load asynchronously', async () => {
104-
// Load the page without waiting for Angular since it is not bootstrapped automatically.
105-
await browser.driver.get(browser.baseUrl);
106-
107-
// Test the contents from the server.
108-
const linkTag = await browser.driver.findElement(
109-
by.css('link[rel="stylesheet"]')
110-
);
111-
expect(await linkTag.getAttribute('media')).toMatch('all');
112-
expect(await linkTag.getAttribute('ngCspMedia')).toBeNull();
113-
expect(await linkTag.getAttribute('onload')).toBeNull();
114-
115-
// Make sure there were no client side errors.
116-
await verifyNoBrowserErrors();
117-
});
118-
119-
it('style tags all have a nonce attribute', async () => {
120-
// Load the page without waiting for Angular since it is not bootstrapped automatically.
121-
await browser.driver.get(browser.baseUrl);
122-
123-
// Test the contents from the server.
124-
for (const s of await browser.driver.findElements(by.css('style'))) {
125-
expect(await s.getAttribute('nonce')).toBe('{% nonce %}');
126-
}
127-
128-
// Make sure there were no client side errors.
129-
await verifyNoBrowserErrors();
130-
});
131-
});
132-
`,
13350
});
13451

13552
async function spawnServer(): Promise<number> {
@@ -158,5 +75,51 @@ export default async function () {
15875
}
15976

16077
const port = await spawnServer();
161-
await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target=');
78+
await executeBrowserTest({
79+
baseUrl: `http://localhost:${port}/`,
80+
checkFn: async (page) => {
81+
// Test the contents from the server.
82+
const h1Text = await page.$eval('h1', (el) => el.textContent);
83+
if (!h1Text?.includes('Hello')) {
84+
throw new Error(`Expected h1 to contain 'Hello', but got '${h1Text}'`);
85+
}
86+
87+
const serverStylePresent = await page.evaluate(
88+
() => !!(globalThis as any).document.querySelector('style[ng-app-id="ng"]'),
89+
);
90+
if (!serverStylePresent) {
91+
throw new Error('Expected server-side style to be present');
92+
}
93+
94+
// style tags all have a nonce attribute
95+
const nonces = await page.$$eval('style', (styles) =>
96+
styles.map((s) => s.getAttribute('nonce')),
97+
);
98+
for (const nonce of nonces) {
99+
if (nonce !== '{% nonce %}') {
100+
throw new Error(`Expected nonce to be '{% nonce %}', but got '${nonce}'`);
101+
}
102+
}
103+
104+
// stylesheets should be configured to load asynchronously
105+
const linkMedia = await page.$eval('link[rel="stylesheet"]', (el) =>
106+
el.getAttribute('media'),
107+
);
108+
if (linkMedia !== 'all') {
109+
throw new Error(`Expected link media to be 'all', but got '${linkMedia}'`);
110+
}
111+
112+
// Bootstrap the client side app.
113+
await page.evaluate('window.doBootstrap()');
114+
115+
// Wait for server style to be removed by client
116+
await page.waitForSelector('style[ng-app-id="ng"]', { hidden: true });
117+
118+
// Retest the contents after the client bootstraps.
119+
const h1TextPost = await page.$eval('h1', (el) => el.textContent);
120+
if (!h1TextPost?.includes('Hello')) {
121+
throw new Error(`Expected h1 to contain 'Hello' after bootstrap, but got '${h1TextPost}'`);
122+
}
123+
},
124+
});
162125
}

0 commit comments

Comments
 (0)