summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/ng-schematics
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/ng-schematics')
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.eslintignore5
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.gitignore3
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs6
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md110
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/README.md230
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/package.json71
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts200
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json26
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts15
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json20
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs4
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts39
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json8
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template18
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts118
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json34
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template2
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template20
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template60
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js4
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts135
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json37
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts152
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts45
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts189
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts47
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts30
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts111
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts260
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts147
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs64
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs159
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs72
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tsconfig.json17
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tsdoc.json15
40 files changed, 2523 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/ng-schematics/.eslintignore b/remote/test/puppeteer/packages/ng-schematics/.eslintignore
new file mode 100644
index 0000000000..8424d7004d
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.eslintignore
@@ -0,0 +1,5 @@
+# Ignore File that will be copied to Angular
+/files/
+
+# Ignore sandbox enviroment
+./sandbox/
diff --git a/remote/test/puppeteer/packages/ng-schematics/.gitignore b/remote/test/puppeteer/packages/ng-schematics/.gitignore
new file mode 100644
index 0000000000..9dad45cdd5
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.gitignore
@@ -0,0 +1,3 @@
+
+# Sandbox
+sandbox/
diff --git a/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs
new file mode 100644
index 0000000000..be9bc29919
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ logLevel: 'debug',
+ spec: 'test/build/**/*.spec.js',
+ exit: !!process.env.CI,
+ reporter: process.env.CI ? 'spec' : 'dot',
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md
new file mode 100644
index 0000000000..a483c4f2fb
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md
@@ -0,0 +1,110 @@
+# Changelog
+
+## [0.5.6](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.5...ng-schematics-v0.5.6) (2024-01-16)
+
+
+### Bug Fixes
+
+* jest config issue on Windows ([3711f86](https://github.com/puppeteer/puppeteer/commit/3711f86dca4140da9e830bd7a46f4eca43cd5f4b))
+
+## [0.5.5](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.4...ng-schematics-v0.5.5) (2023-12-19)
+
+
+### Bug Fixes
+
+* update documentation for ng-schematics ([#11533](https://github.com/puppeteer/puppeteer/issues/11533)) ([744e894](https://github.com/puppeteer/puppeteer/commit/744e8944ac62b9d7284fa260c5c796fa1b83b5ef))
+
+## [0.5.4](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.3...ng-schematics-v0.5.4) (2023-12-06)
+
+
+### Bug Fixes
+
+* get port from created server ([#11495](https://github.com/puppeteer/puppeteer/issues/11495)) ([d2f4b9c](https://github.com/puppeteer/puppeteer/commit/d2f4b9ca53642ac9ccae9a22fd3138698990387b))
+
+## [0.5.3](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.2...ng-schematics-v0.5.3) (2023-12-04)
+
+
+### Bug Fixes
+
+* ng-schematics install Windows ([#11487](https://github.com/puppeteer/puppeteer/issues/11487)) ([02af748](https://github.com/puppeteer/puppeteer/commit/02af7482d9bf2163b90dfe623b0af18c513d5a3b))
+
+## [0.5.2](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.1...ng-schematics-v0.5.2) (2023-11-16)
+
+
+### Bug Fixes
+
+* run post-install hooks ([#11403](https://github.com/puppeteer/puppeteer/issues/11403)) ([3f6ca24](https://github.com/puppeteer/puppeteer/commit/3f6ca249ed898eee25015a6fd0ce7cf774ad31b2))
+
+## [0.5.1](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.0...ng-schematics-v0.5.1) (2023-11-13)
+
+
+### Bug Fixes
+
+* multi-app project extend root `tsconfig.json` ([#11374](https://github.com/puppeteer/puppeteer/issues/11374)) ([1b2d920](https://github.com/puppeteer/puppeteer/commit/1b2d920fe638f3aad704ab8f21d1e4f4099b6d44))
+* support Angular 17 new template ([#11375](https://github.com/puppeteer/puppeteer/issues/11375)) ([64f7bf0](https://github.com/puppeteer/puppeteer/commit/64f7bf0af442369a07352b11555ec3f612eb62b8))
+
+## [0.5.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.4.0...ng-schematics-v0.5.0) (2023-08-22)
+
+
+### Features
+
+* **ng-schematics:** reduce the user options and better defaults ([35dc2d8](https://github.com/puppeteer/puppeteer/commit/35dc2d884052b27a3f9c70b8646f95743be7b84d))
+* **ng-schematics:** release version 0.5.0 ([#10768](https://github.com/puppeteer/puppeteer/issues/10768)) ([42fdd0a](https://github.com/puppeteer/puppeteer/commit/42fdd0a733acb2a9af3878bfa8927252f68ed465))
+
+
+### Bug Fixes
+
+* **ng-schematics:** builder is responsible for resolving commands ([683e181](https://github.com/puppeteer/puppeteer/commit/683e18189c0aedad7deb9007055a1a38801bbf08))
+* **ng-schematics:** don't install for library projects ([1376b77](https://github.com/puppeteer/puppeteer/commit/1376b77a7ab2260c2fd236c3cf31abbd544193e8))
+
+## [0.4.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.4.0) (2023-08-08)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-03)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-02)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.2.0...ng-schematics-v0.3.0) (2023-06-29)
+
+
+### Features
+
+* add Test command ([#10443](https://github.com/puppeteer/puppeteer/issues/10443)) ([2d8993b](https://github.com/puppeteer/puppeteer/commit/2d8993b45b0a0c5943907fe69f865e1064a23d3c))
+
+
+### Bug Fixes
+
+* `port` option to run dev and e2e side-by-side ([#10458](https://github.com/puppeteer/puppeteer/issues/10458)) ([a43b346](https://github.com/puppeteer/puppeteer/commit/a43b346bfc7f0071fcead1abb7d7b46dcf3c27f9))
+* use Node test reporter ([#10464](https://github.com/puppeteer/puppeteer/issues/10464)) ([f778b1e](https://github.com/puppeteer/puppeteer/commit/f778b1e2a70f3d507ab2012d2918f5ed241a8d21))
+
+## [0.2.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.1.0...ng-schematics-v0.2.0) (2023-05-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019))
+
+### Features
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9))
+
+## 0.1.0 (2022-11-23)
+
+
+### Features
+
+* **ng-schematics:** Release @puppeteer/ng-schematics ([#9244](https://github.com/puppeteer/puppeteer/issues/9244)) ([be33929](https://github.com/puppeteer/puppeteer/commit/be33929770e473992ad49029e6d038d36591e108))
diff --git a/remote/test/puppeteer/packages/ng-schematics/README.md b/remote/test/puppeteer/packages/ng-schematics/README.md
new file mode 100644
index 0000000000..975f74a704
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/README.md
@@ -0,0 +1,230 @@
+# Puppeteer Angular Schematic
+
+Adds Puppeteer-based e2e tests to your Angular project.
+
+## Getting started
+
+Run the command below in an Angular CLI app directory and follow the prompts.
+
+> Note this will add the schematic as a dependency to your project.
+
+```bash
+ng add @puppeteer/ng-schematics
+```
+
+Or you can use the same command followed by the [options](#options) below.
+
+Currently, this schematic supports the following test runners:
+
+- [**Jasmine**](https://jasmine.github.io/)
+- [**Jest**](https://jestjs.io/)
+- [**Mocha**](https://mochajs.org/)
+- [**Node Test Runner**](https://nodejs.org/api/test.html)
+
+With the schematics installed you can run E2E tests:
+
+```bash
+ng e2e
+```
+
+### Options
+
+When adding schematics to your project you can to provide following options:
+
+| Option | Description | Value | Required |
+| --------------- | ------------------------------------------------------ | ------------------------------------------ | -------- |
+| `--test-runner` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
+
+## Creating a single test file
+
+Puppeteer Angular Schematic exposes a method to create a single test file.
+
+```bash
+ng generate @puppeteer/ng-schematics:e2e "<TestName>"
+```
+
+### Running test server and dev server at the same time
+
+By default the E2E test will run the app on the same port as `ng start`.
+To avoid this you can specify the port the an the `angular.json`
+Update either `e2e` or `puppeteer` (depending on the initial setup) to:
+
+```json
+{
+ "e2e": {
+ "builder": "@puppeteer/ng-schematics:puppeteer",
+ "options": {
+ "commands": [...],
+ "devServerTarget": "sandbox:serve",
+ "testRunner": "<TestRunner>",
+ "port": 8080
+ },
+ ...
+}
+```
+
+Now update the E2E test file `utils.ts` baseUrl to:
+
+```ts
+const baseUrl = 'http://localhost:8080';
+```
+
+## Contributing
+
+Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of what you need to develop in the Puppeteer repo.
+
+### Sandbox smoke tests
+
+To make integration easier smoke test can be run with a single command, that will create a fresh install of Angular (single application and a milti application projects). Then it will install the schematics inside them and run the initial e2e tests:
+
+```bash
+node tools/smoke.mjs
+```
+
+### Unit Testing
+
+The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:
+
+```bash
+npm run test
+```
+
+## Migrating from Protractor
+
+### Entry point
+
+Puppeteer has its own [`browser`](https://pptr.dev/api/puppeteer.browser) that exposes the browser process.
+A more closes comparison for Protractor's `browser` would be Puppeteer's [`page`](https://pptr.dev/api/puppeteer.page).
+
+```ts
+// Testing framework specific imports
+
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<Test Name>', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ // Query elements
+ await page
+ .locator('my-component')
+ // Click on the element once found
+ .click();
+ });
+});
+```
+
+### Getting element properties
+
+You can easily get any property of the element.
+
+```ts
+// Testing framework specific imports
+
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<Test Name>', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ // Query elements
+ const elementText = await page
+ .locator('.my-component')
+ .map(button => button.innerText)
+ // Wait for element to show up
+ .wait();
+
+ // Assert via assertion library
+ });
+});
+```
+
+### Query Selectors
+
+Puppeteer supports multiple types of selectors, namely, the CSS, ARIA, text, XPath and pierce selectors.
+The following table shows Puppeteer's equivalents to [Protractor By](https://www.protractortest.org/#/api?view=ProtractorBy).
+
+> For improved reliability and reduced flakiness try our
+> **Experimental** [Locators API](https://pptr.dev/guides/locators)
+
+| By | Protractor code | Puppeteer querySelector |
+| ----------------- | --------------------------------------------- | ------------------------------------------------------------ |
+| CSS (Single) | `$(by.css('<CSS>'))` | `page.$('<CSS>')` |
+| CSS (Multiple) | `$$(by.css('<CSS>'))` | `page.$$('<CSS>')` |
+| Id | `$(by.id('<ID>'))` | `page.$('#<ID>')` |
+| CssContainingText | `$(by.cssContainingText('<CSS>', '<TEXT>'))` | `page.$('<CSS> ::-p-text(<TEXT>)')` ` |
+| DeepCss | `$(by.deepCss('<CSS>'))` | `page.$(':scope >>> <CSS>')` |
+| XPath | `$(by.xpath('<XPATH>'))` | `page.$('::-p-xpath(<XPATH>)')` |
+| JS | `$(by.js('document.querySelector("<CSS>")'))` | `page.evaluateHandle(() => document.querySelector('<CSS>'))` |
+
+> For advanced use cases such as Protractor's `by.addLocator` you can check Puppeteer's [Custom selectors](https://pptr.dev/guides/query-selectors#custom-selectors).
+
+### Actions Selectors
+
+Puppeteer allows you to all necessary actions to allow test your application.
+
+```ts
+// Click on the element.
+element(locator).click();
+// Puppeteer equivalent
+await page.locator(locator).click();
+
+// Send keys to the element (usually an input).
+element(locator).sendKeys('my text');
+// Puppeteer equivalent
+await page.locator(locator).fill('my text');
+
+// Clear the text in an element (usually an input).
+element(locator).clear();
+// Puppeteer equivalent
+await page.locator(locator).fill('');
+
+// Get the value of an attribute, for example, get the value of an input.
+element(locator).getAttribute('value');
+// Puppeteer equivalent
+const element = await page.locator(locator).waitHandle();
+const value = await element.getProperty('value');
+```
+
+### Example
+
+Sample Protractor test:
+
+```ts
+describe('Protractor Demo', function () {
+ it('should add one and two', function () {
+ browser.get('http://juliemr.github.io/protractor-demo/');
+ element(by.model('first')).sendKeys(1);
+ element(by.model('second')).sendKeys(2);
+
+ element(by.id('gobutton')).click();
+
+ expect(element(by.binding('latest')).getText()).toEqual('3');
+ });
+});
+```
+
+Sample Puppeteer migration:
+
+```ts
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('Puppeteer Demo', function () {
+ setupBrowserHooks();
+ it('should add one and two', function () {
+ const {page} = getBrowserState();
+ await page.goto('http://juliemr.github.io/protractor-demo/');
+
+ await page.locator('.form-inline > input:nth-child(1)').fill('1');
+ await page.locator('.form-inline > input:nth-child(2)').fill('2');
+ await page.locator('#gobutton').fill('2');
+
+ const result = await page
+ .locator('.table tbody td:last-of-type')
+ .map(header => header.innerText)
+ .wait();
+
+ expect(result).toEqual('3');
+ });
+});
+```
diff --git a/remote/test/puppeteer/packages/ng-schematics/package.json b/remote/test/puppeteer/packages/ng-schematics/package.json
new file mode 100644
index 0000000000..29db1dcdc9
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@puppeteer/ng-schematics",
+ "version": "0.5.6",
+ "description": "Puppeteer Angular schematics",
+ "scripts": {
+ "build": "wireit",
+ "clean": "../../tools/clean.js",
+ "dev:test": "npm run test --watch",
+ "dev": "npm run build --watch",
+ "unit": "wireit"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b && node tools/copySchemaFiles.mjs",
+ "clean": "if-file-deleted",
+ "files": [
+ "tsconfig.json",
+ "tsconfig.test.json",
+ "src/**",
+ "test/src/**"
+ ],
+ "output": [
+ "lib/**",
+ "test/build/**",
+ "*.tsbuildinfo"
+ ]
+ },
+ "build:test": {
+ "command": "tsc -b test/tsconfig.json"
+ },
+ "unit": {
+ "command": "node --test --test-reporter spec test/build",
+ "dependencies": [
+ "build",
+ "build:test"
+ ]
+ }
+ },
+ "keywords": [
+ "angular",
+ "puppeteer",
+ "schematics"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/ng-schematics"
+ },
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.13.2"
+ },
+ "dependencies": {
+ "@angular-devkit/architect": "^0.1701.1",
+ "@angular-devkit/core": "^17.0.7",
+ "@angular-devkit/schematics": "^17.0.7"
+ },
+ "devDependencies": {
+ "@schematics/angular": "^17.0.7",
+ "@angular/cli": "^17.0.7"
+ },
+ "files": [
+ "lib",
+ "!*.tsbuildinfo"
+ ],
+ "ng-add": {
+ "save": "devDependencies"
+ },
+ "schematics": "./lib/schematics/collection.json",
+ "builders": "./lib/builders/builders.json"
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json
new file mode 100644
index 0000000000..41079f7731
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "../../../../node_modules/@angular-devkit/architect/src/builders-schema.json",
+ "builders": {
+ "puppeteer": {
+ "implementation": "./puppeteer",
+ "schema": "./puppeteer/schema.json",
+ "description": "Run e2e test with Puppeteer"
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts
new file mode 100644
index 0000000000..82a1e8e7da
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts
@@ -0,0 +1,200 @@
+import {spawn} from 'child_process';
+import {normalize, join} from 'path';
+
+import {
+ createBuilder,
+ type BuilderContext,
+ type BuilderOutput,
+ targetFromTargetString,
+ type BuilderRun,
+} from '@angular-devkit/architect';
+import type {JsonObject} from '@angular-devkit/core';
+
+import {TestRunner} from '../../schematics/utils/types.js';
+
+import type {PuppeteerBuilderOptions} from './types.js';
+
+const terminalStyles = {
+ cyan: '\u001b[36;1m',
+ green: '\u001b[32m',
+ red: '\u001b[31m',
+ bold: '\u001b[1m',
+ reverse: '\u001b[7m',
+ clear: '\u001b[0m',
+};
+
+export function getCommandForRunner(runner: TestRunner): [string, ...string[]] {
+ switch (runner) {
+ case TestRunner.Jasmine:
+ return [`jasmine`, '--config=./e2e/jasmine.json'];
+ case TestRunner.Jest:
+ return [`jest`, '-c', 'e2e/jest.config.js'];
+ case TestRunner.Mocha:
+ return [`mocha`, '--config=./e2e/.mocharc.js'];
+ case TestRunner.Node:
+ return ['node', '--test', '--test-reporter', 'spec', 'e2e/build/'];
+ }
+
+ throw new Error(`Unknown test runner ${runner}!`);
+}
+
+function getExecutable(command: string[]) {
+ const executable = command.shift()!;
+ const debugError = `Error running '${executable}' with arguments '${command.join(
+ ' '
+ )}'.`;
+
+ return {
+ executable,
+ args: command,
+ debugError,
+ error: 'Please look at the output above to determine the issue!',
+ };
+}
+
+function updateExecutablePath(command: string, root?: string) {
+ if (command === TestRunner.Node) {
+ return command;
+ }
+
+ let path = 'node_modules/.bin/';
+ if (root && root !== '') {
+ const nested = root
+ .split('/')
+ .map(() => {
+ return '../';
+ })
+ .join('');
+ path = `${nested}${path}${command}`;
+ } else {
+ path = `./${path}${command}`;
+ }
+
+ return normalize(path);
+}
+
+async function executeCommand(
+ context: BuilderContext,
+ command: string[],
+ env: NodeJS.ProcessEnv = {}
+) {
+ let project: JsonObject;
+ if (context.target) {
+ project = await context.getProjectMetadata(context.target.project);
+ command[0] = updateExecutablePath(command[0]!, String(project['root']));
+ }
+
+ await new Promise(async (resolve, reject) => {
+ context.logger.debug(`Trying to execute command - ${command.join(' ')}.`);
+ const {executable, args, debugError, error} = getExecutable(command);
+ let path = context.workspaceRoot;
+ if (context.target) {
+ path = join(path, (project['root'] as string | undefined) ?? '');
+ }
+
+ const child = spawn(executable, args, {
+ cwd: path,
+ stdio: 'inherit',
+ shell: true,
+ env: {
+ ...process.env,
+ ...env,
+ },
+ });
+
+ child.on('error', message => {
+ context.logger.debug(debugError);
+ console.log(message);
+ reject(error);
+ });
+
+ child.on('exit', code => {
+ if (code === 0) {
+ resolve(true);
+ } else {
+ reject(error);
+ }
+ });
+ });
+}
+
+function message(
+ message: string,
+ context: BuilderContext,
+ type: 'info' | 'success' | 'error' = 'info'
+): void {
+ let style: string;
+ switch (type) {
+ case 'info':
+ style = terminalStyles.reverse + terminalStyles.cyan;
+ break;
+ case 'success':
+ style = terminalStyles.reverse + terminalStyles.green;
+ break;
+ case 'error':
+ style = terminalStyles.red;
+ break;
+ }
+ context.logger.info(
+ `${terminalStyles.bold}${style}${message}${terminalStyles.clear}`
+ );
+}
+
+async function startServer(
+ options: PuppeteerBuilderOptions,
+ context: BuilderContext
+): Promise<BuilderRun> {
+ context.logger.debug('Trying to start server.');
+ const target = targetFromTargetString(options.devServerTarget);
+ const defaultServerOptions = await context.getTargetOptions(target);
+
+ const overrides = {
+ watch: false,
+ host: defaultServerOptions['host'],
+ port: options.port ?? defaultServerOptions['port'],
+ } as JsonObject;
+
+ message(' Spawning test server ⚙️ ... \n', context);
+ const server = await context.scheduleTarget(target, overrides);
+ const result = await server.result;
+ if (!result.success) {
+ throw new Error('Failed to spawn server! Stopping tests...');
+ }
+
+ return server;
+}
+
+async function executeE2ETest(
+ options: PuppeteerBuilderOptions,
+ context: BuilderContext
+): Promise<BuilderOutput> {
+ let server: BuilderRun | null = null;
+ try {
+ message('\n Building tests 🛠️ ... \n', context);
+ await executeCommand(context, [`tsc`, '-p', 'e2e/tsconfig.json']);
+
+ server = await startServer(options, context);
+ const result = await server.result;
+
+ message('\n Running tests 🧪 ... \n', context);
+ const testRunnerCommand = getCommandForRunner(options.testRunner);
+ await executeCommand(context, testRunnerCommand, {
+ baseUrl: result['baseUrl'],
+ });
+
+ message('\n 🚀 Test ran successfully! 🚀 ', context, 'success');
+ return {success: true};
+ } catch (error) {
+ message('\n 🛑 Test failed! 🛑 ', context, 'error');
+ if (error instanceof Error) {
+ return {success: false, error: error.message};
+ }
+ return {success: false, error: error as string};
+ } finally {
+ if (server) {
+ await server.stop();
+ }
+ }
+}
+
+export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest);
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json
new file mode 100644
index 0000000000..2693d19cce
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json
@@ -0,0 +1,26 @@
+{
+ "title": "Puppeteer",
+ "description": "Options for Puppeteer Angular Schematics",
+ "type": "object",
+ "properties": {
+ "commands": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "item": {
+ "type": "string"
+ }
+ },
+ "description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')."
+ },
+ "devServerTarget": {
+ "type": "string",
+ "description": "Angular target that spawns the server."
+ },
+ "port": {
+ "type": ["number", "null"],
+ "description": "Port to run the test server on."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts
new file mode 100644
index 0000000000..6258a955c0
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JsonObject} from '@angular-devkit/core';
+
+import type {TestRunner} from '../../schematics/utils/types.js';
+
+export interface PuppeteerBuilderOptions extends JsonObject {
+ testRunner: TestRunner;
+ devServerTarget: string;
+ port: number | null;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json
new file mode 100644
index 0000000000..00bede45e5
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
+ "schematics": {
+ "ng-add": {
+ "description": "Add Puppeteer to an Angular project",
+ "factory": "./ng-add/index#ngAdd",
+ "schema": "./ng-add/schema.json"
+ },
+ "e2e": {
+ "description": "Create a single test file",
+ "factory": "./e2e/index#e2e",
+ "schema": "./e2e/schema.json"
+ },
+ "config": {
+ "description": "Eject Puppeteer config file",
+ "factory": "./config/index#config",
+ "schema": "./config/schema.json"
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs
new file mode 100644
index 0000000000..0da14a80d8
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs
@@ -0,0 +1,4 @@
+/**
+ * @type {import("puppeteer").Configuration}
+ */
+export {};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts
new file mode 100644
index 0000000000..b01d98e33e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ type Tree,
+} from '@angular-devkit/schematics';
+
+import {addFilesSingle} from '../utils/files.js';
+import {TestRunner, type AngularProject} from '../utils/types.js';
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function config(): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([addPuppeteerConfig()])(tree, context);
+ };
+}
+
+function addPuppeteerConfig(): Rule {
+ return (_tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer config file.');
+
+ return addFilesSingle('', {root: ''} as AngularProject, {
+ // No-op here to fill types
+ options: {
+ testRunner: TestRunner.Jasmine,
+ port: 4200,
+ },
+ applyPath: './files',
+ relativeToWorkspacePath: `/`,
+ });
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json
new file mode 100644
index 0000000000..8d45751bb1
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer Config Schema",
+ "type": "object",
+ "properties": {},
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template
new file mode 100644
index 0000000000..ca90f258b8
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template
@@ -0,0 +1,18 @@
+<% if(testRunner == 'node') { %>
+import * as assert from 'assert';
+import {describe, it} from 'node:test';
+<% } %><% if(testRunner == 'mocha') { %>
+import * as assert from 'assert';
+<% } %>
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<%= classify(name) %>', function () {
+ <% if(route) { %>
+ setupBrowserHooks('<%= route %>');
+ <% } else { %>
+ setupBrowserHooks();
+ <% } %>
+ it('', async function () {
+ const {page} = getBrowserState();
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts
new file mode 100644
index 0000000000..cf1f634f94
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ SchematicsException,
+ type Tree,
+} from '@angular-devkit/schematics';
+
+import {addCommonFiles} from '../utils/files.js';
+import {getApplicationProjects} from '../utils/json.js';
+import {
+ TestRunner,
+ type SchematicsSpec,
+ type AngularProject,
+ type PuppeteerSchematicsConfig,
+} from '../utils/types.js';
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function e2e(userArgs: Record<string, string>): Rule {
+ const options = parseUserTestArgs(userArgs);
+
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([addE2EFile(options)])(tree, context);
+ };
+}
+
+function parseUserTestArgs(userArgs: Record<string, string>): SchematicsSpec {
+ const options: Partial<SchematicsSpec> = {
+ ...userArgs,
+ };
+ if ('p' in userArgs) {
+ options['project'] = userArgs['p'];
+ }
+ if ('n' in userArgs) {
+ options['name'] = userArgs['n'];
+ }
+ if ('r' in userArgs) {
+ options['route'] = userArgs['r'];
+ }
+
+ if (options['route'] && options['route'].startsWith('/')) {
+ options['route'] = options['route'].substring(1);
+ }
+
+ return options as SchematicsSpec;
+}
+
+function findTestingOption<
+ Property extends keyof PuppeteerSchematicsConfig['options'],
+>(
+ [name, project]: [string, AngularProject | undefined],
+ property: Property
+): PuppeteerSchematicsConfig['options'][Property] {
+ if (!project) {
+ throw new Error(`Project "${name}" not found.`);
+ }
+
+ const e2e = project.architect?.e2e;
+ const puppeteer = project.architect?.puppeteer;
+ const builder = '@puppeteer/ng-schematics:puppeteer';
+
+ if (e2e?.builder === builder) {
+ return e2e.options[property];
+ } else if (puppeteer?.builder === builder) {
+ return puppeteer.options[property];
+ }
+
+ throw new Error(`Can't find property "${property}" for project "${name}".`);
+}
+
+function addE2EFile(options: SchematicsSpec): Rule {
+ return async (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Spec file.');
+
+ const projects = getApplicationProjects(tree);
+ const projectNames = Object.keys(projects) as [string, ...string[]];
+ const foundProject: [string, AngularProject | undefined] | undefined =
+ projectNames.length === 1
+ ? [projectNames[0], projects[projectNames[0]]]
+ : Object.entries(projects).find(([name, project]) => {
+ return options.project
+ ? options.project === name
+ : project.root === '';
+ });
+ if (!foundProject) {
+ throw new SchematicsException(
+ `Project not found! Please run "ng generate @puppeteer/ng-schematics:test <Test> <Project>"`
+ );
+ }
+
+ const testRunner = findTestingOption(foundProject, 'testRunner');
+ const port = findTestingOption(foundProject, 'port');
+
+ context.logger.debug('Creating Spec file.');
+
+ return addCommonFiles(
+ {[foundProject[0]]: foundProject[1]} as Record<string, AngularProject>,
+ {
+ options: {
+ name: options.name,
+ route: options.route,
+ testRunner,
+ // Node test runner does not support glob patterns
+ // It looks for files `*.test.js`
+ ext: testRunner === TestRunner.Node ? 'test' : 'e2e',
+ port,
+ },
+ }
+ );
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json
new file mode 100644
index 0000000000..7752c9ceef
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer E2E Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "alias": "n",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "Name for spec to be created:"
+ },
+ "project": {
+ "type": "string",
+ "$default": {
+ "$source": "argv",
+ "index": 1
+ },
+ "alias": "p"
+ },
+ "route": {
+ "type": "string",
+ "$default": {
+ "$source": "argv",
+ "index": 1
+ },
+ "alias": "r"
+ }
+ },
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template
new file mode 100644
index 0000000000..f038b2eb67
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template
@@ -0,0 +1,2 @@
+# Compiled e2e tests output
+build/
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template
new file mode 100644
index 0000000000..60637d0fa7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template
@@ -0,0 +1,20 @@
+<% if(testRunner == 'node') { %>
+import * as assert from 'assert';
+import {describe, it} from 'node:test';
+<% } %><% if(testRunner == 'mocha') { %>
+import * as assert from 'assert';
+<% } %>
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('App test', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ const element = await page.locator('::-p-text(<%= project %>)').wait();
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ expect(element).not.toBeNull();
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ assert.ok(element);
+<% } %>
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template
new file mode 100644
index 0000000000..2136f99a3a
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template
@@ -0,0 +1,60 @@
+<% if(testRunner == 'node') { %>
+import {before, beforeEach, after, afterEach} from 'node:test';
+<% } %>
+import * as puppeteer from 'puppeteer';
+
+const baseUrl = process.env['baseUrl'] ?? '<%= baseUrl %>';
+let browser: puppeteer.Browser;
+let page: puppeteer.Page;
+
+export function setupBrowserHooks(path = ''): void {
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new'
+ });
+ });
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ before(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new'
+ });
+ });
+<% } %>
+
+ beforeEach(async () => {
+ page = await browser.newPage();
+ await page.goto(`${baseUrl}${path}`);
+ });
+
+ afterEach(async () => {
+ await page?.close();
+ });
+
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ afterAll(async () => {
+ await browser?.close();
+ });
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ after(async () => {
+ await browser?.close();
+ });
+<% } %>
+}
+
+export function getBrowserState(): {
+ browser: puppeteer.Browser;
+ page: puppeteer.Page;
+ baseUrl: string;
+} {
+ if (!browser) {
+ throw new Error(
+ 'No browser state found! Ensure `setupBrowserHooks()` is called.'
+ );
+ }
+ return {
+ browser,
+ page,
+ baseUrl,
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template
new file mode 100644
index 0000000000..38501b89ef
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template
@@ -0,0 +1,10 @@
+{
+ "extends": "<%= tsConfigPath %>",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "rootDir": "tests/",
+ "outDir": "build/",
+ "types": ["<%= testRunner %>"]
+ },
+ "include": ["tests/**/*.ts"]
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json
new file mode 100644
index 0000000000..ad5dc6fbce
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json
@@ -0,0 +1,10 @@
+{
+ "spec_dir": "e2e",
+ "spec_files": ["**/*[eE]2[eE].js"],
+ "helpers": ["helpers/**/*.?(m)js"],
+ "env": {
+ "failSpecWithNoExpectations": true,
+ "stopSpecOnExpectationFailure": false,
+ "random": true
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js
new file mode 100644
index 0000000000..ee21c6737e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js
@@ -0,0 +1,10 @@
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/configuration
+ */
+
+/** @type {import('jest').Config} */
+module.exports = {
+ testMatch: ['<rootDir>/build/**/*.e2e.js'],
+ testEnvironment: 'node',
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js
new file mode 100644
index 0000000000..28c1839674
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js
@@ -0,0 +1,4 @@
+module.exports = {
+ spec: './e2e/build/**/*.e2e.js',
+ timeout: 5000,
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts
new file mode 100644
index 0000000000..1f962e0cfc
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts
@@ -0,0 +1,135 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ type Tree,
+} from '@angular-devkit/schematics';
+import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
+import {of} from 'rxjs';
+import {concatMap, map, scan} from 'rxjs/operators';
+
+import {
+ addCommonFiles as addCommonFilesHelper,
+ addFrameworkFiles,
+ getNgCommandName,
+ hasE2ETester,
+} from '../utils/files.js';
+import {getApplicationProjects} from '../utils/json.js';
+import {
+ addPackageJsonDependencies,
+ addPackageJsonScripts,
+ getDependenciesFromOptions,
+ getPackageLatestNpmVersion,
+ DependencyType,
+ type NodePackage,
+ updateAngularJsonScripts,
+} from '../utils/packages.js';
+import {TestRunner, type SchematicsOptions} from '../utils/types.js';
+
+const DEFAULT_PORT = 4200;
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function ngAdd(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([
+ addDependencies(options),
+ addCommonFiles(options),
+ addOtherFiles(options),
+ updateScripts(),
+ updateAngularConfig(options),
+ ])(tree, context);
+ };
+}
+
+function addDependencies(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding dependencies to "package.json"');
+ const dependencies = getDependenciesFromOptions(options);
+
+ return of(...dependencies).pipe(
+ concatMap((packageName: string) => {
+ return getPackageLatestNpmVersion(packageName);
+ }),
+ scan((array, nodePackage) => {
+ array.push(nodePackage);
+ return array;
+ }, [] as NodePackage[]),
+ map(packages => {
+ context.logger.debug('Updating dependencies...');
+ addPackageJsonDependencies(tree, packages, DependencyType.Dev);
+ context.addTask(
+ new NodePackageInstallTask({
+ // Trigger Post-Install hooks to download the browser
+ allowScripts: true,
+ })
+ );
+
+ return tree;
+ })
+ );
+ };
+}
+
+function updateScripts(): Rule {
+ return (tree: Tree, context: SchematicContext): Tree => {
+ context.logger.debug('Updating "package.json" scripts');
+ const projects = getApplicationProjects(tree);
+ const projectsKeys = Object.keys(projects);
+
+ if (projectsKeys.length === 1) {
+ const name = getNgCommandName(projects);
+ const prefix = hasE2ETester(projects) ? `run ${projectsKeys[0]}:` : '';
+ return addPackageJsonScripts(tree, [
+ {
+ name,
+ script: `ng ${prefix}${name}`,
+ },
+ ]);
+ }
+ return tree;
+ };
+}
+
+function addCommonFiles(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer base files.');
+ const projects = getApplicationProjects(tree);
+
+ return addCommonFilesHelper(projects, {
+ options: {
+ ...options,
+ port: DEFAULT_PORT,
+ ext: options.testRunner === TestRunner.Node ? 'test' : 'e2e',
+ },
+ });
+ };
+}
+
+function addOtherFiles(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer additional files.');
+ const projects = getApplicationProjects(tree);
+
+ return addFrameworkFiles(projects, {
+ options: {
+ ...options,
+ port: DEFAULT_PORT,
+ },
+ });
+ };
+}
+
+function updateAngularConfig(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext): Tree => {
+ context.logger.debug('Updating "angular.json".');
+
+ return updateAngularJsonScripts(tree, options);
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json
new file mode 100644
index 0000000000..0fa581f1a7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer Install Schema",
+ "type": "object",
+ "properties": {
+ "testRunner": {
+ "type": "string",
+ "enum": ["jasmine", "jest", "mocha", "node"],
+ "default": "jasmine",
+ "alias": "t",
+ "x-prompt": {
+ "message": "Which test runners do you wish to use?",
+ "type": "list",
+ "items": [
+ {
+ "value": "jasmine",
+ "label": "Use Jasmine [https://jasmine.github.io/]"
+ },
+ {
+ "value": "jest",
+ "label": "Use Jest [https://jestjs.io/]"
+ },
+ {
+ "value": "mocha",
+ "label": "Use Mocha [https://mochajs.org/]"
+ },
+ {
+ "value": "node",
+ "label": "Use Node Test Runner [https://nodejs.org/api/test.html]"
+ }
+ ]
+ }
+ }
+ },
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts
new file mode 100644
index 0000000000..4d255062b4
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {relative, resolve} from 'path';
+
+import {getSystemPath, normalize, strings} from '@angular-devkit/core';
+import type {Rule} from '@angular-devkit/schematics';
+import {
+ apply,
+ applyTemplates,
+ chain,
+ mergeWith,
+ move,
+ url,
+} from '@angular-devkit/schematics';
+
+import type {AngularProject, TestRunner} from './types.js';
+
+export interface FilesOptions {
+ options: {
+ testRunner: TestRunner;
+ port: number;
+ name?: string;
+ exportConfig?: boolean;
+ ext?: string;
+ route?: string;
+ };
+ applyPath: string;
+ relativeToWorkspacePath: string;
+ movePath?: string;
+}
+
+export function addFilesToProjects(
+ projects: Record<string, AngularProject>,
+ options: FilesOptions
+): Rule {
+ return chain(
+ Object.keys(projects).map(name => {
+ return addFilesSingle(name, projects[name] as AngularProject, options);
+ })
+ );
+}
+
+export function addFilesSingle(
+ name: string,
+ project: AngularProject,
+ {options, applyPath, movePath, relativeToWorkspacePath}: FilesOptions
+): Rule {
+ const projectPath = resolve(getSystemPath(normalize(project.root)));
+ const workspacePath = resolve(getSystemPath(normalize('')));
+
+ const relativeToWorkspace = relative(
+ `${projectPath}${relativeToWorkspacePath}`,
+ workspacePath
+ );
+
+ const baseUrl = getProjectBaseUrl(project, options.port);
+ const tsConfigPath = getTsConfigPath(project);
+
+ return mergeWith(
+ apply(url(applyPath), [
+ move(movePath ? `${project.root}${movePath}` : project.root),
+ applyTemplates({
+ ...options,
+ ...strings,
+ root: project.root ? `${project.root}/` : project.root,
+ baseUrl,
+ tsConfigPath,
+ project: name,
+ relativeToWorkspace,
+ }),
+ ])
+ );
+}
+
+function getProjectBaseUrl(project: AngularProject, port: number): string {
+ let options = {protocol: 'http', port, host: 'localhost'};
+
+ if (project.architect?.serve?.options) {
+ const projectOptions = project.architect?.serve?.options;
+ const projectPort = port !== 4200 ? port : projectOptions?.port ?? port;
+ options = {...options, ...projectOptions, port: projectPort};
+ options.protocol = projectOptions.ssl ? 'https' : 'http';
+ }
+
+ return `${options.protocol}://${options.host}:${options.port}/`;
+}
+
+function getTsConfigPath(project: AngularProject): string {
+ const filename = 'tsconfig.json';
+
+ if (!project.root) {
+ return `../${filename}`;
+ }
+
+ const nested = project.root
+ .split('/')
+ .map(() => {
+ return '../';
+ })
+ .join('');
+
+ // Prepend a single `../` as we put the test inside `e2e` folder
+ return `../${nested}${filename}`;
+}
+
+export function addCommonFiles(
+ projects: Record<string, AngularProject>,
+ filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
+): Rule {
+ const options: FilesOptions = {
+ ...filesOptions,
+ applyPath: './files/common',
+ relativeToWorkspacePath: `/`,
+ };
+
+ return addFilesToProjects(projects, options);
+}
+
+export function addFrameworkFiles(
+ projects: Record<string, AngularProject>,
+ filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
+): Rule {
+ const testRunner = filesOptions.options.testRunner;
+ const options: FilesOptions = {
+ ...filesOptions,
+ applyPath: `./files/${testRunner}`,
+ relativeToWorkspacePath: `/`,
+ };
+
+ return addFilesToProjects(projects, options);
+}
+
+export function hasE2ETester(
+ projects: Record<string, AngularProject>
+): boolean {
+ return Object.values(projects).some((project: AngularProject) => {
+ return Boolean(project.architect?.e2e);
+ });
+}
+
+export function getNgCommandName(
+ projects: Record<string, AngularProject>
+): string {
+ if (!hasE2ETester(projects)) {
+ return 'e2e';
+ }
+ return 'puppeteer';
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts
new file mode 100644
index 0000000000..1a38d638a7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {SchematicsException, type Tree} from '@angular-devkit/schematics';
+
+import type {AngularJson, AngularProject} from './types.js';
+
+export function getJsonFileAsObject(
+ tree: Tree,
+ path: string
+): Record<string, unknown> {
+ try {
+ const buffer = tree.read(path) as Buffer;
+ const content = buffer.toString();
+ return JSON.parse(content);
+ } catch {
+ throw new SchematicsException(`Unable to retrieve file at ${path}.`);
+ }
+}
+
+export function getObjectAsJson(object: Record<string, unknown>): string {
+ return JSON.stringify(object, null, 2);
+}
+
+export function getAngularConfig(tree: Tree): AngularJson {
+ return getJsonFileAsObject(tree, './angular.json') as unknown as AngularJson;
+}
+
+export function getApplicationProjects(
+ tree: Tree
+): Record<string, AngularProject> {
+ const {projects} = getAngularConfig(tree);
+
+ const applications: Record<string, AngularProject> = {};
+ for (const key in projects) {
+ const project = projects[key]!;
+ if (project.projectType === 'application') {
+ applications[key] = project;
+ }
+ }
+ return applications;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts
new file mode 100644
index 0000000000..6ef8ef6002
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts
@@ -0,0 +1,189 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {get} from 'https';
+
+import type {Tree} from '@angular-devkit/schematics';
+
+import {getNgCommandName} from './files.js';
+import {
+ getAngularConfig,
+ getApplicationProjects,
+ getJsonFileAsObject,
+ getObjectAsJson,
+} from './json.js';
+import {type SchematicsOptions, TestRunner} from './types.js';
+export interface NodePackage {
+ name: string;
+ version: string;
+}
+export interface NodeScripts {
+ name: string;
+ script: string;
+}
+
+export enum DependencyType {
+ Default = 'dependencies',
+ Dev = 'devDependencies',
+ Peer = 'peerDependencies',
+ Optional = 'optionalDependencies',
+}
+
+export function getPackageLatestNpmVersion(name: string): Promise<NodePackage> {
+ return new Promise(resolve => {
+ let version = 'latest';
+
+ return get(`https://registry.npmjs.org/${name}`, res => {
+ let data = '';
+
+ res.on('data', chunk => {
+ data += chunk;
+ });
+ res.on('end', () => {
+ try {
+ const response = JSON.parse(data);
+ version = response?.['dist-tags']?.latest ?? version;
+ } catch {
+ } finally {
+ resolve({
+ name,
+ version,
+ });
+ }
+ });
+ }).on('error', () => {
+ resolve({
+ name,
+ version,
+ });
+ });
+ });
+}
+
+function updateJsonValues(
+ json: Record<string, any>,
+ target: string,
+ updates: Array<{name: string; value: any}>,
+ overwrite = false
+) {
+ updates.forEach(({name, value}) => {
+ if (!json[target][name] || overwrite) {
+ json[target] = {
+ ...json[target],
+ [name]: value,
+ };
+ }
+ });
+}
+
+export function addPackageJsonDependencies(
+ tree: Tree,
+ packages: NodePackage[],
+ type: DependencyType,
+ overwrite?: boolean,
+ fileLocation = './package.json'
+): Tree {
+ const packageJson = getJsonFileAsObject(tree, fileLocation);
+
+ updateJsonValues(
+ packageJson,
+ type,
+ packages.map(({name, version}) => {
+ return {name, value: version};
+ }),
+ overwrite
+ );
+
+ tree.overwrite(fileLocation, getObjectAsJson(packageJson));
+
+ return tree;
+}
+
+export function getDependenciesFromOptions(
+ options: SchematicsOptions
+): string[] {
+ const dependencies = ['puppeteer'];
+
+ switch (options.testRunner) {
+ case TestRunner.Jasmine:
+ dependencies.push('jasmine');
+ break;
+ case TestRunner.Jest:
+ dependencies.push('jest', '@types/jest');
+ break;
+ case TestRunner.Mocha:
+ dependencies.push('mocha', '@types/mocha');
+ break;
+ case TestRunner.Node:
+ dependencies.push('@types/node');
+ break;
+ }
+
+ return dependencies;
+}
+
+export function addPackageJsonScripts(
+ tree: Tree,
+ scripts: NodeScripts[],
+ overwrite?: boolean,
+ fileLocation = './package.json'
+): Tree {
+ const packageJson = getJsonFileAsObject(tree, fileLocation);
+
+ updateJsonValues(
+ packageJson,
+ 'scripts',
+ scripts.map(({name, script}) => {
+ return {name, value: script};
+ }),
+ overwrite
+ );
+
+ tree.overwrite(fileLocation, getObjectAsJson(packageJson));
+
+ return tree;
+}
+
+export function updateAngularJsonScripts(
+ tree: Tree,
+ options: SchematicsOptions,
+ overwrite = true
+): Tree {
+ const angularJson = getAngularConfig(tree);
+ const projects = getApplicationProjects(tree);
+ const name = getNgCommandName(projects);
+
+ Object.keys(projects).forEach(project => {
+ const e2eScript = [
+ {
+ name,
+ value: {
+ builder: '@puppeteer/ng-schematics:puppeteer',
+ options: {
+ devServerTarget: `${project}:serve`,
+ testRunner: options.testRunner,
+ },
+ configurations: {
+ production: {
+ devServerTarget: `${project}:serve:production`,
+ },
+ },
+ },
+ },
+ ];
+
+ updateJsonValues(
+ angularJson['projects'][project]!,
+ 'architect',
+ e2eScript,
+ overwrite
+ );
+ });
+
+ tree.overwrite('./angular.json', getObjectAsJson(angularJson as any));
+
+ return tree;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts
new file mode 100644
index 0000000000..7d66e0f0fa
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export enum TestRunner {
+ Jasmine = 'jasmine',
+ Jest = 'jest',
+ Mocha = 'mocha',
+ Node = 'node',
+}
+
+export interface SchematicsOptions {
+ testRunner: TestRunner;
+}
+
+export interface PuppeteerSchematicsConfig {
+ builder: string;
+ options: {
+ port: number;
+ testRunner: TestRunner;
+ };
+}
+export interface AngularProject {
+ projectType: 'application' | 'library';
+ root: string;
+ architect: {
+ e2e?: PuppeteerSchematicsConfig;
+ puppeteer?: PuppeteerSchematicsConfig;
+ serve: {
+ options: {
+ ssl: string;
+ port: number;
+ };
+ };
+ };
+}
+export interface AngularJson {
+ projects: Record<string, AngularProject>;
+}
+
+export interface SchematicsSpec {
+ name: string;
+ project?: string;
+ route?: string;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts
new file mode 100644
index 0000000000..e4ec03ed54
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts
@@ -0,0 +1,30 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ buildTestingTree,
+ getMultiApplicationFile,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: config', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('config', 'single');
+ expect(tree.files).toContain('/.puppeteerrc.mjs');
+ });
+ });
+
+ void describe('Multi projects', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('config', 'multi');
+ expect(tree.files).toContain('/.puppeteerrc.mjs');
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('.puppeteerrc.mjs')
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts
new file mode 100644
index 0000000000..8ae211cd59
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts
@@ -0,0 +1,111 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ buildTestingTree,
+ getMultiApplicationFile,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: e2e', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.files).not.toContain('/e2e/tests/my-test.test.ts');
+ });
+
+ void it('should create Node file', async () => {
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ testRunner: 'node',
+ });
+ expect(tree.files).not.toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/my-test.test.ts');
+ });
+
+ void it('should create file with route', async () => {
+ const route = 'home';
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
+ `setupBrowserHooks('${route}');`
+ );
+ });
+
+ void it('should create with route with starting slash', async () => {
+ const route = '/home';
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
+ `setupBrowserHooks('home');`
+ );
+ });
+ });
+
+ void describe('Multi projects', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/my-test.test.ts')
+ );
+ });
+
+ void it('should create Node file', async () => {
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ testRunner: 'node',
+ });
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.test.ts')
+ );
+ });
+
+ void it('should create file with route', async () => {
+ const route = 'home';
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(
+ tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
+ ).toContain(`setupBrowserHooks('${route}');`);
+ });
+
+ void it('should create with route with starting slash', async () => {
+ const route = '/home';
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(
+ tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
+ ).toContain(`setupBrowserHooks('home');`);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts
new file mode 100644
index 0000000000..d912c5dc3d
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts
@@ -0,0 +1,260 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ MULTI_LIBRARY_OPTIONS,
+ buildTestingTree,
+ getAngularJsonScripts,
+ getMultiApplicationFile,
+ getMultiLibraryFile,
+ getPackageJson,
+ runSchematic,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: ng-add', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add');
+ const {devDependencies, scripts} = getPackageJson(tree);
+ const {builder, configurations} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/tsconfig.json');
+ expect(tree.files).toContain('/e2e/tests/app.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/utils.ts');
+ expect(devDependencies).toContain('puppeteer');
+ expect(scripts['e2e']).toBe('ng e2e');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ expect(configurations).toEqual({
+ production: {
+ devServerTarget: 'sandbox:serve:production',
+ },
+ });
+ });
+ void it('should update create proper "ng" command for non default tester', async () => {
+ let tree = await buildTestingTree('ng-add', 'single');
+ // Re-run schematic to have e2e populated
+ tree = await runSchematic(tree, 'ng-add');
+ const {scripts} = getPackageJson(tree);
+ const {builder} = getAngularJsonScripts(tree, false);
+
+ expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ });
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'single');
+
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ void it('should create Jasmine files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'jasmine',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/jasmine.json');
+ expect(devDependencies).toContain('jasmine');
+ expect(options['testRunner']).toBe('jasmine');
+ });
+ void it('should create Jest files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'jest',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/jest.config.js');
+ expect(devDependencies).toContain('jest');
+ expect(devDependencies).toContain('@types/jest');
+ expect(options['testRunner']).toBe('jest');
+ });
+ void it('should create Mocha files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'mocha',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/.mocharc.js');
+ expect(devDependencies).toContain('mocha');
+ expect(devDependencies).toContain('@types/mocha');
+ expect(options['testRunner']).toBe('mocha');
+ });
+ void it('should create Node files', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'node',
+ });
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/.gitignore');
+ expect(tree.files).not.toContain('/e2e/tests/app.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/app.test.ts');
+ expect(options['testRunner']).toBe('node');
+ });
+ void it('should create TypeScript files', async () => {
+ const tree = await buildTestingTree('ng-add', 'single');
+ const tsConfigPath = '/e2e/tsconfig.json';
+ const tsConfig = tree.readJson(tsConfigPath);
+
+ expect(tree.files).toContain(tsConfigPath);
+ expect(tsConfig).toMatchObject({
+ extends: '../tsconfig.json',
+ compilerOptions: {
+ module: 'CommonJS',
+ },
+ });
+ });
+ void it('should not create port value', async () => {
+ const tree = await buildTestingTree('ng-add');
+
+ const {options} = getAngularJsonScripts(tree);
+ expect(options['port']).toBeUndefined();
+ });
+ });
+
+ void describe('Multi projects Application', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const {devDependencies, scripts} = getPackageJson(tree);
+ const {builder, configurations} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tsconfig.json')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/utils.ts')
+ );
+ expect(devDependencies).toContain('puppeteer');
+ expect(scripts['e2e']).toBe('ng e2e');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ expect(configurations).toEqual({
+ production: {
+ devServerTarget: 'sandbox:serve:production',
+ },
+ });
+ });
+ void it('should update create proper "ng" command for non default tester', async () => {
+ let tree = await buildTestingTree('ng-add', 'multi');
+ // Re-run schematic to have e2e populated
+ tree = await runSchematic(tree, 'ng-add');
+ const {scripts} = getPackageJson(tree);
+ const {builder} = getAngularJsonScripts(tree, false);
+
+ expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ });
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'multi');
+
+ expect(files).not.toContain(getMultiApplicationFile('.puppeteerrc.cjs'));
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ void it('should create Jasmine files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'jasmine',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/jasmine.json'));
+ expect(devDependencies).toContain('jasmine');
+ expect(options['testRunner']).toBe('jasmine');
+ });
+ void it('should create Jest files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'jest',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/jest.config.js')
+ );
+ expect(devDependencies).toContain('jest');
+ expect(devDependencies).toContain('@types/jest');
+ expect(options['testRunner']).toBe('jest');
+ });
+ void it('should create Mocha files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'mocha',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/.mocharc.js'));
+ expect(devDependencies).toContain('mocha');
+ expect(devDependencies).toContain('@types/mocha');
+ expect(options['testRunner']).toBe('mocha');
+ });
+ void it('should create Node files', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'node',
+ });
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/.gitignore'));
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/app.test.ts')
+ );
+ expect(options['testRunner']).toBe('node');
+ });
+ void it('should create TypeScript files', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const tsConfigPath = getMultiApplicationFile('e2e/tsconfig.json');
+ const tsConfig = tree.readJson(tsConfigPath);
+
+ expect(tree.files).toContain(tsConfigPath);
+ expect(tsConfig).toMatchObject({
+ extends: '../../../tsconfig.json',
+ compilerOptions: {
+ module: 'CommonJS',
+ },
+ });
+ });
+ void it('should not create port value', async () => {
+ const tree = await buildTestingTree('ng-add');
+
+ const {options} = getAngularJsonScripts(tree);
+ expect(options['port']).toBeUndefined();
+ });
+ });
+
+ void describe('Multi projects Library', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const config = getAngularJsonScripts(
+ tree,
+ true,
+ MULTI_LIBRARY_OPTIONS.name
+ );
+
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tsconfig.json')
+ );
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tests/utils.ts')
+ );
+ expect(config).toBeUndefined();
+ });
+
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'multi');
+
+ expect(files).not.toContain(getMultiLibraryFile('.puppeteerrc.cjs'));
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts
new file mode 100644
index 0000000000..503cbd5cec
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts
@@ -0,0 +1,147 @@
+import https from 'https';
+import {before, after} from 'node:test';
+import {join} from 'path';
+
+import type {JsonObject} from '@angular-devkit/core';
+import {
+ SchematicTestRunner,
+ type UnitTestTree,
+} from '@angular-devkit/schematics/testing';
+import sinon from 'sinon';
+
+const WORKSPACE_OPTIONS = {
+ name: 'workspace',
+ newProjectRoot: 'projects',
+ version: '14.0.0',
+};
+
+const SINGLE_APPLICATION_OPTIONS = {
+ name: 'sandbox',
+ directory: '.',
+ createApplication: true,
+ version: '14.0.0',
+};
+
+const MULTI_APPLICATION_OPTIONS = {
+ name: SINGLE_APPLICATION_OPTIONS.name,
+};
+
+export const MULTI_LIBRARY_OPTIONS = {
+ name: 'components',
+};
+
+export function setupHttpHooks(): void {
+ // Stop outgoing Request for version fetching
+ before(() => {
+ const httpsGetStub = sinon.stub(https, 'get');
+ httpsGetStub.returns({
+ on: (_: string, callback: () => void) => {
+ callback();
+ },
+ } as any);
+ });
+
+ after(() => {
+ sinon.restore();
+ });
+}
+
+export function getAngularJsonScripts(
+ tree: UnitTestTree,
+ isDefault = true,
+ name = SINGLE_APPLICATION_OPTIONS.name
+): {
+ builder: string;
+ configurations: Record<string, any>;
+ options: Record<string, any>;
+} {
+ const angularJson = tree.readJson('angular.json') as any;
+ const e2eScript = isDefault ? 'e2e' : 'puppeteer';
+ return angularJson['projects']?.[name]?.['architect'][e2eScript];
+}
+
+export function getPackageJson(tree: UnitTestTree): {
+ scripts: Record<string, string>;
+ devDependencies: string[];
+} {
+ const packageJson = tree.readJson('package.json') as JsonObject;
+ return {
+ scripts: packageJson['scripts'] as any,
+ devDependencies: Object.keys(
+ packageJson['devDependencies'] as Record<string, string>
+ ),
+ };
+}
+
+export function getMultiApplicationFile(file: string): string {
+ return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_APPLICATION_OPTIONS.name}/${file}`;
+}
+export function getMultiLibraryFile(file: string): string {
+ return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_LIBRARY_OPTIONS.name}/${file}`;
+}
+
+export async function buildTestingTree(
+ command: 'ng-add' | 'e2e' | 'config',
+ type: 'single' | 'multi' = 'single',
+ userOptions?: Record<string, unknown>
+): Promise<UnitTestTree> {
+ const runner = new SchematicTestRunner(
+ 'schematics',
+ join(__dirname, '../../lib/schematics/collection.json')
+ );
+ const options = {
+ testRunner: 'jasmine',
+ ...userOptions,
+ };
+ let workingTree: UnitTestTree;
+
+ // Build workspace
+ if (type === 'single') {
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'ng-new',
+ SINGLE_APPLICATION_OPTIONS
+ );
+ } else {
+ // Build workspace
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'workspace',
+ WORKSPACE_OPTIONS
+ );
+ // Build dummy application
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'application',
+ MULTI_APPLICATION_OPTIONS,
+ workingTree
+ );
+ // Build dummy library
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'library',
+ MULTI_LIBRARY_OPTIONS,
+ workingTree
+ );
+ }
+
+ if (command !== 'ng-add') {
+ // We want to create update the proper files with `ng-add`
+ // First else the angular.json will have wrong data
+ workingTree = await runner.runSchematic('ng-add', options, workingTree);
+ }
+
+ return await runner.runSchematic(command, options, workingTree);
+}
+
+export async function runSchematic(
+ tree: UnitTestTree,
+ command: 'ng-add' | 'test',
+ options?: Record<string, any>
+): Promise<UnitTestTree> {
+ const runner = new SchematicTestRunner(
+ 'schematics',
+ join(__dirname, '../../lib/schematics/collection.json')
+ );
+ return await runner.runSchematic(command, options, tree);
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json b/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json
new file mode 100644
index 0000000000..3d45f9cc54
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src/",
+ "outDir": "build/",
+ "types": ["node"],
+ },
+ "include": ["src/**/*"],
+ "references": [{"path": "../tsconfig.json"}],
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs
new file mode 100644
index 0000000000..2bd88f229a
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs/promises';
+import path from 'path';
+import url from 'url';
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
+
+/**
+ *
+ * @param {String} directory
+ * @param {String[]} files
+ */
+async function findSchemaFiles(directory, files = []) {
+ const items = await fs.readdir(directory);
+ const promises = [];
+ // Match any listing that has no *.* format
+ // Ignore files folder
+ const regEx = /^.*\.[^\s]*$/;
+
+ items.forEach(item => {
+ if (!item.match(regEx)) {
+ promises.push(findSchemaFiles(`${directory}/${item}`, files));
+ } else if (item.endsWith('.json') || directory.includes('files')) {
+ files.push(`${directory}/${item}`);
+ }
+ });
+
+ await Promise.all(promises);
+
+ return files;
+}
+
+async function copySchemaFiles() {
+ const srcDir = path.join(__dirname, '..', 'src');
+ const outputDir = path.join(__dirname, '..', 'lib');
+ const files = await findSchemaFiles(srcDir);
+
+ const moves = files.map(file => {
+ const to = file.replace(srcDir, outputDir);
+
+ return {from: file, to};
+ });
+
+ // Because fs.cp is Experimental (recursive support)
+ // We need to create directories first and copy the files
+ await Promise.all(
+ moves.map(({to}) => {
+ const dir = path.dirname(to);
+ return fs.mkdir(dir, {recursive: true});
+ })
+ );
+ await Promise.all(
+ moves.map(({from, to}) => {
+ return fs.copyFile(from, to);
+ })
+ );
+}
+
+copySchemaFiles();
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs
new file mode 100644
index 0000000000..985200881e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs
@@ -0,0 +1,159 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {spawn} from 'child_process';
+import {randomUUID} from 'crypto';
+import {readFile, writeFile} from 'fs/promises';
+import {join} from 'path';
+import {cwd} from 'process';
+
+class AngularProject {
+ static ports = new Set();
+ static randomPort() {
+ const min = 4000;
+ const max = 9876;
+ return Math.floor(Math.random() * (max - min + 1) + min);
+ }
+ static port() {
+ const port = AngularProject.randomPort();
+ if (AngularProject.ports.has(port)) {
+ return AngularProject.port();
+ }
+ return port;
+ }
+
+ static #scripts = testRunner => {
+ return {
+ // Builds the ng-schematics before running them
+ 'build:schematics': 'npm run --prefix ../../ build',
+ // Deletes all files created by Puppeteer Ng-Schematics to avoid errors
+ 'delete:file':
+ 'rm -f .puppeteerrc.cjs && rm -f tsconfig.e2e.json && rm -R -f e2e/',
+ // Runs the Puppeteer Ng-Schematics against the sandbox
+ schematics: 'schematics ../../:ng-add --dry-run=false',
+ 'schematics:e2e': 'schematics ../../:e2e --dry-run=false',
+ 'schematics:config': 'schematics ../../:config --dry-run=false',
+ 'schematics:smoke': `schematics ../../:ng-add --dry-run=false --test-runner="${testRunner}" && ng e2e`,
+ };
+ };
+ /** Folder name */
+ #name;
+ /** E2E test runner to use */
+ #runner;
+
+ constructor(runner, name) {
+ this.#runner = runner ?? 'node';
+ this.#name = name ?? randomUUID();
+ }
+
+ get runner() {
+ return this.#runner;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ async executeCommand(command, options) {
+ const [executable, ...args] = command.split(' ');
+ await new Promise((resolve, reject) => {
+ const createProcess = spawn(executable, args, {
+ shell: true,
+ ...options,
+ });
+
+ createProcess.stdout.on('data', data => {
+ data = data
+ .toString()
+ // Replace new lines with a prefix including the test runner
+ .replace(/(?:\r\n?|\n)(?=.*[\r\n])/g, `\n${this.#runner} - `);
+ console.log(`${this.#runner} - ${data}`);
+ });
+
+ createProcess.on('error', message => {
+ console.error(`Running ${command} exited with error:`, message);
+ reject(message);
+ });
+
+ createProcess.on('exit', code => {
+ if (code === 0) {
+ resolve(true);
+ } else {
+ reject();
+ }
+ });
+ });
+ }
+
+ async create() {
+ await this.createProject();
+ await this.updatePackageJson();
+ }
+
+ async updatePackageJson() {
+ const packageJsonFile = join(cwd(), `/sandbox/${this.#name}/package.json`);
+ const packageJson = JSON.parse(await readFile(packageJsonFile));
+ packageJson['scripts'] = {
+ ...packageJson['scripts'],
+ ...AngularProject.#scripts(this.#runner),
+ };
+ await writeFile(packageJsonFile, JSON.stringify(packageJson, null, 2));
+ }
+
+ get commandOptions() {
+ return {
+ ...process.env,
+ cwd: join(cwd(), `/sandbox/${this.#name}/`),
+ };
+ }
+
+ async runNpmScripts(command) {
+ await this.executeCommand(`npm run ${command}`, this.commandOptions);
+ }
+
+ async runSchematics() {
+ await this.runNpmScripts('schematics');
+ }
+
+ async runSchematicsE2E() {
+ await this.runNpmScripts('schematics:e2e');
+ }
+
+ async runSchematicsConfig() {
+ await this.runNpmScripts('schematics:config');
+ }
+
+ async runSmoke() {
+ await this.runNpmScripts(
+ `schematics:smoke -- --port=${AngularProject.port()}`
+ );
+ }
+}
+
+export class AngularProjectSingle extends AngularProject {
+ async createProject() {
+ await this.executeCommand(
+ `ng new ${this.name} --directory=sandbox/${this.name} --defaults --skip-git`
+ );
+ }
+}
+
+export class AngularProjectMulti extends AngularProject {
+ async createProject() {
+ await this.executeCommand(
+ `ng new ${this.name} --create-application=false --directory=sandbox/${this.name} --defaults --skip-git`
+ );
+
+ await this.executeCommand(
+ `ng generate application core --style=css --routing=true`,
+ this.commandOptions
+ );
+ await this.executeCommand(
+ `ng generate application admin --style=css --routing=false`,
+ this.commandOptions
+ );
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs
new file mode 100644
index 0000000000..8ae9907266
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ok} from 'node:assert';
+import {execSync} from 'node:child_process';
+import {parseArgs} from 'node:util';
+
+import {AngularProjectMulti, AngularProjectSingle} from './projects.mjs';
+
+const {values: args} = parseArgs({
+ options: {
+ testRunner: {
+ type: 'string',
+ short: 't',
+ default: undefined,
+ },
+ name: {
+ type: 'string',
+ short: 'n',
+ default: undefined,
+ },
+ },
+});
+
+if (process.env.CI) {
+ // Need to install in CI
+ execSync('npm install -g @angular/cli@latest @angular-devkit/schematics-cli');
+ const runners = ['node', 'jest', 'jasmine', 'mocha'];
+ const groups = [];
+
+ for (const runner of runners) {
+ groups.push([
+ new AngularProjectSingle(runner),
+ new AngularProjectMulti(runner),
+ ]);
+ }
+
+ const angularProjects = await Promise.allSettled(
+ groups.flat().map(async project => {
+ return await project.create();
+ })
+ );
+ ok(
+ angularProjects.every(project => {
+ return project.status === 'fulfilled';
+ }),
+ 'Building of 1 or more projects failed!'
+ );
+
+ for await (const runnerGroup of groups) {
+ const smokeResults = await Promise.allSettled(
+ runnerGroup.map(async project => {
+ return await project.runSmoke();
+ })
+ );
+ ok(
+ smokeResults.every(project => {
+ return project.status === 'fulfilled';
+ }),
+ `Smoke test for ${runnerGroup[0].runner} failed!`
+ );
+ }
+} else {
+ const single = new AngularProjectSingle(args.testRunner, args.name);
+ const multi = new AngularProjectMulti(args.testRunner, args.name);
+
+ await Promise.all([single.create(), multi.create()]);
+ await Promise.all([single.runSmoke(), multi.runSmoke()]);
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tsconfig.json b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json
new file mode 100644
index 0000000000..40529c7d17
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "tsconfig",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noEmitOnError": true,
+ "rootDir": "src/",
+ "outDir": "lib/",
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "types": ["node"],
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/files/**/*"],
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tsdoc.json b/remote/test/puppeteer/packages/ng-schematics/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tsdoc.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+
+ "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"],
+ "tagDefinitions": [
+ {
+ "tagName": "@license",
+ "syntaxKind": "modifier",
+ "allowMultiple": false
+ }
+ ],
+ "supportForTags": {
+ "@license": true
+ }
+}