Skip to content
Draft
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
40 changes: 21 additions & 19 deletions tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getGlobalVariable } from '../../../utils/env';
import { appendToFile, expectFileToMatch, writeFile } from '../../../utils/fs';
import { appendToFile, expectFileToMatch } from '../../../utils/fs';
import { installPackage } from '../../../utils/packages';
import { ng } from '../../../utils/process';
import { updateJsonFile } from '../../../utils/project';
import { executeBrowserTest } from '../../../utils/puppeteer';

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

Expand Down Expand Up @@ -31,26 +32,27 @@ export default async function () {
}
}

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

it('should have ngsw in normal state', () => {
browser.get('/');
await executeBrowserTest({
configuration: 'production',
checkFn: async (page) => {
// Wait for service worker to load.
browser.sleep(2000);
browser.waitForAngularEnabled(false);
browser.get('/ngsw/state');
// Should have updated, and be in normal state.
expect(element(by.css('pre')).getText()).not.toContain('Last update check: never');
expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL');
});
`,
);
await new Promise((resolve) => setTimeout(resolve, 2000));
await page.waitForSelector('h1');

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

await ng('e2e', '--configuration=production');
// Should have updated, and be in normal state.
const preText = await page.$eval('pre', (el) => el.textContent);
if (preText?.includes('Last update check: never')) {
throw new Error(`Expected service worker to have checked for updates, but got: ${preText}`);
}
if (!preText?.includes('Driver state: NORMAL')) {
throw new Error(`Expected service worker driver state to be NORMAL, but got: ${preText}`);
}
},
});
}
97 changes: 46 additions & 51 deletions tests/e2e/tests/build/auto-csp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import assert from 'node:assert';
import { setTimeout } from 'node:timers/promises';
import { getGlobalVariable } from '../../utils/env';
import { expectFileToMatch, writeFile, writeMultipleFiles } from '../../utils/fs';
import { findFreePort } from '../../utils/network';
import { execAndWaitForOutputToMatch, ng } from '../../utils/process';
import { updateJsonFile } from '../../utils/project';
import { executeBrowserTest } from '../../utils/puppeteer';

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

Expand Down Expand Up @@ -67,55 +69,6 @@ export default async function () {
</body>
</html>
`,
'e2e/src/app.e2e-spec.ts': `
import { browser, by, element } from 'protractor';
import * as webdriver from 'selenium-webdriver';

function allConsoleWarnMessagesAndErrors() {
return browser
.manage()
.logs()
.get('browser')
.then(function (browserLog: any[]) {
const warnMessages: any[] = [];
browserLog.filter((logEntry) => {
const msg = logEntry.message;
console.log('>> ' + msg);
if (logEntry.level.value >= webdriver.logging.Level.INFO.value) {
warnMessages.push(msg);
}
});
return warnMessages;
});
}

describe('Hello world E2E Tests', () => {
beforeAll(async () => {
await browser.waitForAngularEnabled(true);
});

it('should display: Welcome and run all scripts in order', async () => {
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await browser.driver.get(browser.baseUrl);

// Test the contents.
expect(await element(by.css('h1')).getText()).toMatch('Hello');

// Make sure all scripts ran and there were no client side errors.
const consoleMessages = await allConsoleWarnMessagesAndErrors();
expect(consoleMessages.length).toEqual(4); // No additional errors
// Extract just the printed messages from the console data.
const printedMessages = consoleMessages.map(m => m.match(/"(.*?)"/)[1]);
expect(printedMessages).toEqual([
// All messages printed in order because execution order is preserved.
"Inline Script Head",
"Inline Script Body: 1339",
"First External Script: 1338",
"Second External Script: 1337",
]);
});
});
`,
});

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

// Make sure that our e2e protractor tests run to confirm that our angular project runs.
// Make sure that our e2e tests run to confirm that our angular project runs.
const port = await spawnServer();
await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target=');
await executeBrowserTest({
baseUrl: `http://localhost:${port}/`,
checkFn: async (page) => {
const warnMessages: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'warning') {
warnMessages.push(msg.text());
}
});

// Reload to ensure we capture messages from the start if needed,
// although executeBrowserTest already navigated.
await page.reload();

// Wait for the expected number of warnings
let retries = 50;
while (warnMessages.length < 4 && retries > 0) {
await setTimeout(100);
retries--;
}

if (warnMessages.length !== 4) {
throw new Error(
`Expected 4 console warnings, but got ${warnMessages.length}:\n${warnMessages.join('\n')}`,
);
}

const expectedMessages = [
'Inline Script Head',
'Inline Script Body: 1339',
'First External Script: 1338',
'Second External Script: 1337',
];

for (let i = 0; i < expectedMessages.length; i++) {
if (!warnMessages[i].includes(expectedMessages[i])) {
throw new Error(
`Expected warning ${i} to include '${expectedMessages[i]}', but got '${warnMessages[i]}'`,
);
}
}
},
});
}
133 changes: 48 additions & 85 deletions tests/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { findFreePort } from '../../../utils/network';
import { installWorkspacePackages } from '../../../utils/packages';
import { execAndWaitForOutputToMatch, ng } from '../../../utils/process';
import { updateJsonFile, updateServerFileForEsbuild, useSha } from '../../../utils/project';
import { executeBrowserTest } from '../../../utils/puppeteer';

export default async function () {
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
Expand Down Expand Up @@ -46,90 +47,6 @@ export default async function () {
</body>
</html>
`,
'e2e/src/app.e2e-spec.ts': `
import { browser, by, element } from 'protractor';
import * as webdriver from 'selenium-webdriver';

function verifyNoBrowserErrors() {
return browser
.manage()
.logs()
.get('browser')
.then(function (browserLog: any[]) {
const errors: any[] = [];
browserLog.filter((logEntry) => {
const msg = logEntry.message;
console.log('>> ' + msg);
if (logEntry.level.value >= webdriver.logging.Level.INFO.value) {
errors.push(msg);
}
});
expect(errors).toEqual([]);
});
}

describe('Hello world E2E Tests', () => {
beforeAll(async () => {
await browser.waitForAngularEnabled(false);
});

it('should display: Welcome', async () => {
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await browser.driver.get(browser.baseUrl);

expect(
await element(by.css('style[ng-app-id="ng"]')).getText()
).not.toBeNull();

// Test the contents from the server.
expect(await element(by.css('h1')).getText()).toMatch('Hello');

// Bootstrap the client side app.
await browser.executeScript('doBootstrap()');

// Retest the contents after the client bootstraps.
expect(await element(by.css('h1')).getText()).toMatch('Hello');

// Make sure the server styles got replaced by client side ones.
expect(
await element(by.css('style[ng-app-id="ng"]')).isPresent()
).toBeFalsy();
expect(await element(by.css('style')).getText()).toMatch('');

// Make sure there were no client side errors.
await verifyNoBrowserErrors();
});

it('stylesheets should be configured to load asynchronously', async () => {
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await browser.driver.get(browser.baseUrl);

// Test the contents from the server.
const linkTag = await browser.driver.findElement(
by.css('link[rel="stylesheet"]')
);
expect(await linkTag.getAttribute('media')).toMatch('all');
expect(await linkTag.getAttribute('ngCspMedia')).toBeNull();
expect(await linkTag.getAttribute('onload')).toBeNull();

// Make sure there were no client side errors.
await verifyNoBrowserErrors();
});

it('style tags all have a nonce attribute', async () => {
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await browser.driver.get(browser.baseUrl);

// Test the contents from the server.
for (const s of await browser.driver.findElements(by.css('style'))) {
expect(await s.getAttribute('nonce')).toBe('{% nonce %}');
}

// Make sure there were no client side errors.
await verifyNoBrowserErrors();
});
});
`,
});

async function spawnServer(): Promise<number> {
Expand Down Expand Up @@ -158,5 +75,51 @@ export default async function () {
}

const port = await spawnServer();
await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target=');
await executeBrowserTest({
baseUrl: `http://localhost:${port}/`,
checkFn: async (page) => {
// Test the contents from the server.
const h1Text = await page.$eval('h1', (el) => el.textContent);
if (!h1Text?.includes('Hello')) {
throw new Error(`Expected h1 to contain 'Hello', but got '${h1Text}'`);
}

const serverStylePresent = await page.evaluate(
() => !!(globalThis as any).document.querySelector('style[ng-app-id="ng"]'),
);
if (!serverStylePresent) {
throw new Error('Expected server-side style to be present');
}

// style tags all have a nonce attribute
const nonces = await page.$$eval('style', (styles) =>
styles.map((s) => s.getAttribute('nonce')),
);
for (const nonce of nonces) {
if (nonce !== '{% nonce %}') {
throw new Error(`Expected nonce to be '{% nonce %}', but got '${nonce}'`);
}
}

// stylesheets should be configured to load asynchronously
const linkMedia = await page.$eval('link[rel="stylesheet"]', (el) =>
el.getAttribute('media'),
);
if (linkMedia !== 'all') {
throw new Error(`Expected link media to be 'all', but got '${linkMedia}'`);
}

// Bootstrap the client side app.
await page.evaluate('window.doBootstrap()');

// Wait for server style to be removed by client
await page.waitForSelector('style[ng-app-id="ng"]', { hidden: true });

// Retest the contents after the client bootstraps.
const h1TextPost = await page.$eval('h1', (el) => el.textContent);
if (!h1TextPost?.includes('Hello')) {
throw new Error(`Expected h1 to contain 'Hello' after bootstrap, but got '${h1TextPost}'`);
}
},
});
}
Loading
Loading