├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── biome.json
├── docs
└── playwright-schematics.gif
├── jest.config.ts
├── package-lock.json
├── package.json
├── src
├── builders
│ ├── builders.json
│ └── playwright
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── schema.json
└── schematics
│ ├── collection.json
│ ├── e2e
│ ├── __snapshots__
│ │ └── index.spec.ts.snap
│ ├── files
│ │ └── __name@dasherize__.spec.ts.template
│ ├── index.spec.ts
│ ├── index.ts
│ └── schema.json
│ ├── install-browsers
│ └── index.ts
│ └── ng-add
│ ├── files
│ ├── e2e
│ │ ├── example.spec.ts
│ │ └── tsconfig.json
│ └── playwright.config.ts
│ ├── index.spec.ts
│ ├── index.ts
│ └── schema.json
└── tsconfig.json
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches: [ "main" ]
5 | pull_request:
6 | branches: [ "main" ]
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node-version: [18, 20]
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | cache: 'npm'
20 | - run: npm ci
21 | - run: npm run build
22 | - run: npm run lint
23 | - run: npm test
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | coverage
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | The changelog is available on the [releases page](https://github.com/playwright-community/playwright-ng-schematics/releases)
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Playwright Angular Schematic
2 |
3 | [](https://www.npmjs.com/package/playwright-ng-schematics)
4 | [](https://playwright.dev/)
5 | [](https://github.com/mxschmitt/awesome-playwright)
6 |
7 | Adds [Playwright Test](https://playwright.dev/) to your Angular project
8 |
9 | - Installs Playwright Test
10 | - Set up `ng e2e` for you
11 | - Adds configuration to `angular.json` for easy integration into your existing project
12 | - `ng generate` e2e tests
13 |
14 |
15 |
16 | ## Installation
17 |
18 | Run the following to add Playwright to your Angular project. `ng add` will pick the correct version of this schematic automatically
19 | ```bash
20 | ng add playwright-ng-schematics
21 | ```
22 |
23 | Once installed, you can run the tests
24 | ```bash
25 | npm run e2e
26 | ```
27 |
28 | ## Requirements
29 |
30 | Angular 18+
31 |
32 | ## Usage
33 |
34 | ### Run tests
35 |
36 | You can also use the Angular CLI `ng` to run your tests
37 | ```bash
38 | ng e2e
39 | ```
40 |
41 | You can use almost the same command-line interface options that exist for Playwright (see [Playwright Docs](https://playwright.dev/docs/test-cli) or use `ng e2e --help`), such as UI mode
42 | ```bash
43 | ng e2e --ui
44 | # or
45 | npm run e2e -- --ui
46 | ```
47 |
48 | To specify particular test files, usually done like this `npx playwright test tests/todo-page/ tests/landing-page/`, you have to prepend the `--files` argument.
49 | ```bash
50 | ng e2e --files tests/todo-page/ --files tests/landing-page/
51 | ```
52 | The `-c` option is used to choose an Angular configuration. If you also want to specify a Playwright configuration, use `--config` instead.
53 |
54 | ### Start an Angular development server
55 |
56 | If a `devServerTarget` option is specified, the builder will launch an Angular server and will automatically set the `PLAYWRIGHT_TEST_BASE_URL` environment variable.
57 |
58 | ```json title="angular.json"
59 | "e2e": {
60 | "builder": "playwright-ng-schematics:playwright",
61 | "options": {
62 | "devServerTarget": "my-app:serve",
63 | "ui": true
64 | },
65 | "configurations": {
66 | "production": {
67 | "devServerTarget": "my-app:serve:production"
68 | }
69 | }
70 | }
71 | ```
72 |
73 | You still can make use of Playwright's `baseURL` option and mix it with `PLAYWRIGHT_TEST_BASE_URL` env variable.
74 | The example below shows projects using `PLAYWRIGHT_TEST_BASE_URL` (set by `devServerTarget`) or another base URL.
75 |
76 | ```ts title="playwright.config.ts"
77 | // ...
78 | projects: [
79 | {
80 | name: 'chromium',
81 | use: { ...devices['Desktop Chrome'], baseURL: process.env['PLAYWRIGHT_TEST_BASE_URL'] },
82 | },
83 |
84 | {
85 | name: 'firefox',
86 | use: { ...devices['Desktop Firefox'], baseURL: 'http://example.com' },
87 | },
88 | ]
89 | ```
90 |
91 | ### Create a test file
92 |
93 | Create a new empty test
94 | ```bash
95 | ng generate playwright-ng-schematics:e2e ""
96 | ```
97 |
98 | or with CLI prompt of the name
99 | ```bash
100 | ng generate playwright-ng-schematics:e2e
101 | ```
102 |
103 | ## Migrating from Protractor
104 |
105 | Still using Protractor ?
106 |
107 | Read the [Migrating from Protractor](https://playwright.dev/docs/protractor) guide on the official Playwright website.
108 |
109 | ## Contribute
110 |
111 | - Small, incremental changes are easier to review.
112 | - Conventional Commits. NO EMOJI
113 |
114 | ## License
115 |
116 | This project is licensed under an Apache-2.0 license.
117 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatter": {
3 | "ignore": ["package.json"],
4 | "indentStyle": "space"
5 | },
6 | "javascript": {
7 | "formatter": {
8 | "quoteStyle": "single"
9 | }
10 | },
11 | "linter": {
12 | "ignore": ["src/*/files/**/*"]
13 | },
14 | "vcs": {
15 | "enabled": true,
16 | "clientKind": "git",
17 | "useIgnoreFile": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/playwright-schematics.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/playwright-community/playwright-ng-schematics/b374016c363d867e62415d5574bfe2661817b6af/docs/playwright-schematics.gif
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { JestConfigWithTsJest } from 'ts-jest';
2 |
3 | const config: JestConfigWithTsJest = {
4 | testEnvironment: 'node',
5 |
6 | rootDir: 'src',
7 | testPathIgnorePatterns: ['.*/files/'],
8 | transform: {
9 | '^.+.ts$': ['ts-jest', {}],
10 | },
11 | };
12 |
13 | export default config;
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright-ng-schematics",
3 | "version": "2.1.0",
4 | "description": "Playwright Angular schematics",
5 | "scripts": {
6 | "build": "tsc -p tsconfig.json",
7 | "postbuild": "cpy \"src/**/*.json\" \"src/**/files/**/*\" lib",
8 | "lint": "biome ci",
9 | "lint:fix": "biome check --write",
10 | "test": "jest",
11 | "test:ci": "jest --runInBand"
12 | },
13 | "files": [
14 | "lib",
15 | "CHANGELOG.md"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/playwright-community/playwright-ng-schematics.git"
20 | },
21 | "license": "Apache-2.0",
22 | "keywords": [
23 | "angular",
24 | "e2e",
25 | "playwright",
26 | "schematics",
27 | "testing"
28 | ],
29 | "dependencies": {
30 | "@angular-devkit/architect": ">= 0.2000.0 < 0.2100.0",
31 | "@angular-devkit/core": "^20.0.0",
32 | "@angular-devkit/schematics": "^20.0.0"
33 | },
34 | "devDependencies": {
35 | "@biomejs/biome": "^1.9.4",
36 | "@schematics/angular": "^20.0.0",
37 | "@types/jest": "^29.5.14",
38 | "@types/node": "^20.17.23",
39 | "cpy-cli": "^5.0.0",
40 | "jest": "^29.7.0",
41 | "ts-jest": "^29.2.6",
42 | "ts-node": "^10.9.2",
43 | "typescript": "^5.8.3"
44 | },
45 | "peerDependencies": {
46 | "@angular-devkit/architect": ">= 0.2000.0 < 0.2100.0",
47 | "@angular-devkit/core": "^20.0.0",
48 | "@angular-devkit/schematics": "^20.0.0"
49 | },
50 | "builders": "./lib/builders/builders.json",
51 | "schematics": "./lib/schematics/collection.json",
52 | "ng-add": {
53 | "save": "devDependencies"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/builders/builders.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json",
3 | "builders": {
4 | "playwright": {
5 | "implementation": "./playwright",
6 | "schema": "./playwright/schema.json",
7 | "description": "Run Playwright Test"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/builders/playwright/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from 'node:child_process';
2 | import { join } from 'node:path';
3 | import { Architect } from '@angular-devkit/architect';
4 | import {
5 | type BuilderOutput,
6 | createBuilder,
7 | targetFromTargetString,
8 | } from '@angular-devkit/architect';
9 | import { TestingArchitectHost } from '@angular-devkit/architect/testing';
10 |
11 | jest.mock('node:child_process');
12 |
13 | describe('Playwright builder', () => {
14 | let architect: Architect;
15 |
16 | beforeEach(async () => {
17 | const architectHost = new TestingArchitectHost();
18 | architect = new Architect(architectHost);
19 | await architectHost.addBuilderFromPackage(join(__dirname, '../../..'));
20 |
21 | // Builder that mocks `ng run app:serve`
22 | const fakeBuilder = (): BuilderOutput => {
23 | return { success: true, baseUrl: 'https://example.com' };
24 | };
25 | architectHost.addBuilder('fakeBuilder', createBuilder(fakeBuilder));
26 | architectHost.addTarget(targetFromTargetString('app:serve'), 'fakeBuilder');
27 |
28 | (spawn as jest.Mock).mockReturnValue({
29 | on: jest.fn((event, callback) => callback(0)),
30 | });
31 | });
32 |
33 | afterEach(() => {
34 | jest.clearAllMocks();
35 | });
36 |
37 | it('should spawn testing process', async () => {
38 | const run = await architect.scheduleBuilder(
39 | 'playwright-ng-schematics:playwright',
40 | {},
41 | );
42 | await run.stop();
43 | const output = await run.result;
44 |
45 | expect(output.success).toBeTruthy();
46 | expect(spawn).toHaveBeenCalledWith(
47 | 'npx playwright test',
48 | [],
49 | expect.anything(),
50 | );
51 | });
52 |
53 | it('should spawn testing process and Angular server', async () => {
54 | const run = await architect.scheduleBuilder(
55 | 'playwright-ng-schematics:playwright',
56 | {
57 | devServerTarget: 'app:serve',
58 | },
59 | );
60 | await run.stop();
61 | const output = await run.result;
62 |
63 | expect(output.success).toBeTruthy();
64 | expect(spawn).toHaveBeenCalledWith(
65 | 'npx playwright test',
66 | [],
67 | expect.objectContaining({
68 | env: expect.objectContaining({
69 | PLAYWRIGHT_TEST_BASE_URL: 'https://example.com',
70 | }),
71 | }),
72 | );
73 | });
74 |
75 | it('should fail on error', async () => {
76 | (spawn as jest.Mock).mockReturnValue({
77 | on: jest.fn((event, callback) => callback(-3)),
78 | });
79 |
80 | const run = await architect.scheduleBuilder(
81 | 'playwright-ng-schematics:playwright',
82 | {},
83 | );
84 | await run.stop();
85 | const output = await run.result;
86 |
87 | expect(output.success).toBeFalsy();
88 | });
89 |
90 | it('should accept --ui option', async () => {
91 | const run = await architect.scheduleBuilder(
92 | 'playwright-ng-schematics:playwright',
93 | { ui: true },
94 | );
95 | await run.stop();
96 | const output = await run.result;
97 |
98 | expect(spawn).toHaveBeenCalledWith(
99 | 'npx playwright test',
100 | ['--ui'],
101 | expect.anything(),
102 | );
103 | expect(output.success).toBeTruthy();
104 | });
105 |
106 | it('should accept unknown options', async () => {
107 | const run = await architect.scheduleBuilder(
108 | 'playwright-ng-schematics:playwright',
109 | {
110 | test1: 'testValue',
111 | test2: 2,
112 | test3: true,
113 | test4: [],
114 | test5: {},
115 | test6: null,
116 | a: 'yes',
117 | b: true,
118 | },
119 | );
120 | await run.stop();
121 | const output = await run.result;
122 |
123 | expect(spawn).toHaveBeenCalledWith(
124 | 'npx playwright test',
125 | ['--test1', 'testValue', '--test2', '2', '--test3', '-a', 'yes', '-b'],
126 | expect.anything(),
127 | );
128 | expect(output.success).toBeTruthy();
129 | });
130 |
131 | it('should accept --files option', async () => {
132 | const run = await architect.scheduleBuilder(
133 | 'playwright-ng-schematics:playwright',
134 | {
135 | 'fail-on-flaky-tests': true,
136 | files: ['tests/todo-page/', 'tests/landing-page/'],
137 | },
138 | );
139 | await run.stop();
140 | const output = await run.result;
141 |
142 | expect(spawn).toHaveBeenCalledWith(
143 | 'npx playwright test',
144 | ['tests/todo-page/', 'tests/landing-page/', '--fail-on-flaky-tests'],
145 | expect.anything(),
146 | );
147 | expect(output.success).toBeTruthy();
148 | });
149 |
150 | it('should convert camelCase options to kebab-case', async () => {
151 | const run = await architect.scheduleBuilder(
152 | 'playwright-ng-schematics:playwright',
153 | { updateSnapshots: true },
154 | );
155 | await run.stop();
156 | const output = await run.result;
157 |
158 | expect(spawn).toHaveBeenCalledWith(
159 | 'npx playwright test',
160 | ['--update-snapshots'],
161 | expect.anything(),
162 | );
163 | expect(output.success).toBeTruthy();
164 | });
165 | });
166 |
--------------------------------------------------------------------------------
/src/builders/playwright/index.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from 'node:child_process';
2 | import {
3 | type BuilderContext,
4 | type BuilderOutput,
5 | type BuilderRun,
6 | createBuilder,
7 | targetFromTargetString,
8 | } from '@angular-devkit/architect';
9 | import { type JsonObject, strings } from '@angular-devkit/core';
10 |
11 | /**
12 | * Converts the options object back to an argv string array.
13 | *
14 | * @example
15 | * buildArgs({"workers": 2}); // returns ["--workers", 2]
16 | */
17 | function buildArgs(options: JsonObject): string[] {
18 | // extract files
19 | const filesArgs = (options.files as string[]) ?? [];
20 | options.files = null;
21 |
22 | return [
23 | ...filesArgs,
24 | ...Object.entries(options).flatMap(([key, value]) => {
25 | // Skip builder-internal options
26 | if (key === 'devServerTarget') {
27 | return [];
28 | }
29 |
30 | // Skip objects, arrays, null, undefined (should already be validated by Angular though)
31 | if (
32 | typeof value === 'object' ||
33 | Array.isArray(value) ||
34 | value === null ||
35 | value === undefined
36 | ) {
37 | return [];
38 | }
39 |
40 | // options automatically got converted to camelCase, so we have to convert them back to kebab-case for Playwright.
41 | const dashes = key.length === 1 ? '-' : '--';
42 | const argument = `${dashes}${strings.dasherize(key)}`;
43 |
44 | if (typeof value === 'boolean') {
45 | if (value) {
46 | return argument;
47 | }
48 | return [];
49 | }
50 | return [argument, String(value)];
51 | }),
52 | ];
53 | }
54 |
55 | async function startDevServer(
56 | context: BuilderContext,
57 | devServerTarget: string,
58 | ): Promise {
59 | const target = targetFromTargetString(devServerTarget);
60 | const server = await context.scheduleTarget(target, {});
61 |
62 | return server;
63 | }
64 |
65 | async function startPlaywrightTest(options: JsonObject, baseURL: string) {
66 | // PLAYWRIGHT_TEST_BASE_URL is actually a non-documented env variable used
67 | // by Playwright Test.
68 | // Its usage in playwright.config.ts is to clarify that it can be overriden.
69 | let env = process.env;
70 | if (baseURL) {
71 | env = {
72 | PLAYWRIGHT_TEST_BASE_URL: baseURL,
73 | ...process.env,
74 | };
75 | }
76 |
77 | return new Promise((resolve, reject) => {
78 | const childProcess = spawn('npx playwright test', buildArgs(options), {
79 | cwd: process.cwd(),
80 | stdio: 'inherit',
81 | shell: true,
82 | env,
83 | });
84 |
85 | childProcess.on('exit', (exitCode) => {
86 | if (exitCode !== 0) {
87 | reject(exitCode);
88 | }
89 | resolve(true);
90 | });
91 | });
92 | }
93 |
94 | async function runE2E(
95 | options: JsonObject,
96 | context: BuilderContext,
97 | ): Promise {
98 | let server: BuilderRun | undefined = undefined;
99 | let baseURL = '';
100 |
101 | try {
102 | if (
103 | options.devServerTarget &&
104 | typeof options.devServerTarget === 'string'
105 | ) {
106 | server = await startDevServer(context, options.devServerTarget);
107 | const result = await server.result;
108 | baseURL = result.baseUrl;
109 | }
110 |
111 | await startPlaywrightTest(options, baseURL);
112 | return { success: true };
113 | } catch (error) {
114 | return { success: false };
115 | } finally {
116 | if (server) {
117 | server.stop();
118 | }
119 | }
120 | }
121 |
122 | export default createBuilder(runE2E);
123 |
--------------------------------------------------------------------------------
/src/builders/playwright/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "title": "Playwright",
4 | "description": "Playwright builder options",
5 | "type": "object",
6 | "properties": {
7 | "devServerTarget": {
8 | "description": "Dev server target to run tests against",
9 | "type": "string"
10 | },
11 | "files": {
12 | "description": "Run a test file with the given file name or in the given directory. To specify multiple names, repeat this argument.",
13 | "type": "array",
14 | "items": {
15 | "type": "string"
16 | }
17 | },
18 |
19 | "config": {
20 | "description": "Configuration file. If not passed, defaults to `playwright.config.ts` or `playwright.config.js` in the current directory.",
21 | "type": "string"
22 | },
23 | "debug": {
24 | "description": "Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.",
25 | "type": "boolean"
26 | },
27 | "fail-on-flaky-tests": {
28 | "description": "Fails test runs that contain flaky tests. By default flaky tests count as successes.",
29 | "type": "boolean"
30 | },
31 | "forbid-only": {
32 | "description": "Whether to disallow `test.only`. Useful on CI.",
33 | "type": "boolean"
34 | },
35 | "global-timeout": {
36 | "description": "Total timeout for the whole test run in milliseconds. By default, there is no global timeout.",
37 | "type": "number"
38 | },
39 | "grep": {
40 | "description": "Only run tests matching this regular expression.",
41 | "type": "string",
42 | "alias": "g"
43 | },
44 | "grep-invert": {
45 | "description": "Only run tests not matching this regular expression. The opposite of `--grep`. The filter does not apply to the tests from dependency projects, i.e. Playwright will still run all tests from project dependencies.",
46 | "type": "string"
47 | },
48 | "headed": {
49 | "description": "Run tests in headed browsers. Useful for debugging.",
50 | "type": "boolean"
51 | },
52 | "ignore-snapshots": {
53 | "description": "Whether to ignore snapshots. Use this when snapshot expectations are known to be different, e.g. running tests on Linux against Windows screenshots.",
54 | "type": "boolean"
55 | },
56 | "last-failed": {
57 | "description": "Only re-run the failures.",
58 | "type": "boolean"
59 | },
60 | "list": {
61 | "description": "List all the tests, but do not run them.",
62 | "type": "boolean"
63 | },
64 | "max-failures": {
65 | "description": "Stop after the first `N` test failures.",
66 | "type": "number"
67 | },
68 | "x": {
69 | "description": "Stop after the first failure.",
70 | "type": "boolean"
71 | },
72 | "no-deps": {
73 | "description": "Ignore the dependencies between projects and behave as if they were not specified.",
74 | "type": "boolean"
75 | },
76 | "output": {
77 | "description": "Directory for artifacts produced by tests, defaults to `test-results`.",
78 | "type": "string"
79 | },
80 | "only-changed": {
81 | "description": "Only run test files that have been changed between working tree and \"ref\". Defaults to running all uncommitted changes with ref=HEAD. Only supports Git.",
82 | "type": "string"
83 | },
84 | "pass-with-no-tests": {
85 | "description": "Allows the test suite to pass when no files are found.",
86 | "type": "boolean"
87 | },
88 | "project": {
89 | "description": "Only run tests from the specified projects, supports '*' wildcard. Defaults to running all projects defined in the configuration file.",
90 | "type": "string"
91 | },
92 | "quiet": {
93 | "description": "Whether to suppress stdout and stderr from the tests.",
94 | "type": "boolean"
95 | },
96 | "repeat-each": {
97 | "description": "Run each test `N` times, defaults to one.",
98 | "type": "number"
99 | },
100 | "reporter": {
101 | "description": "Choose a reporter: minimalist `dot`, concise `line` or detailed `list`.",
102 | "enum": ["dot", "line", "list"]
103 | },
104 | "retries": {
105 | "description": "The maximum number of retries for flaky tests, defaults to zero (no retries).",
106 | "type": "number"
107 | },
108 | "shard": {
109 | "description": "Shard tests and execute only selected shard, specified in the form `current/all`, 1-based, for example `3/5`.",
110 | "type": "string",
111 | "pattern": "^\\d+\\/\\d+$"
112 | },
113 | "timeout": {
114 | "description": "Maximum timeout in milliseconds for each test, defaults to 30 seconds.",
115 | "type": "number"
116 | },
117 | "trace": {
118 | "description": "Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure`",
119 | "enum": [
120 | "on",
121 | "off",
122 | "on-first-retry",
123 | "on-all-retries",
124 | "retain-on-failure",
125 | "retain-on-first-failure"
126 | ]
127 | },
128 | "tsconfig": {
129 | "description": "Path to a single tsconfig applicable to all imported files.",
130 | "type": "string"
131 | },
132 | "ui": {
133 | "description": "Run tests in interactive UI mode, with a built-in watch mode.",
134 | "type": "boolean"
135 | },
136 | "update-snapshots": {
137 | "description": "Whether to update snapshots with actual results instead of comparing them. Use this when snapshot expectations have changed.",
138 | "type": "boolean",
139 | "alias": "u"
140 | },
141 | "workers": {
142 | "description": "The maximum number of concurrent worker processes that run in parallel.",
143 | "type": "number",
144 | "alias": "j"
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/schematics/collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
3 | "schematics": {
4 | "ng-add": {
5 | "description": "Adds Playwright Test to your Angular project",
6 | "factory": "./ng-add/index",
7 | "schema": "./ng-add/schema.json"
8 | },
9 | "install-browsers": {
10 | "description": "Install Browsers",
11 | "factory": "./install-browsers/index"
12 | },
13 | "e2e": {
14 | "description": "Creates a single test file",
15 | "factory": "./e2e/index",
16 | "schema": "./e2e/schema.json"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/schematics/e2e/__snapshots__/index.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`e2e should generate spec file 1`] = `
4 | "import { test, expect } from '@playwright/test';
5 |
6 | test.describe('Hello', () => {
7 | test('', async ({ page }) => {
8 |
9 | });
10 | });
11 | "
12 | `;
13 |
--------------------------------------------------------------------------------
/src/schematics/e2e/files/__name@dasherize__.spec.ts.template:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test.describe('<%= classify(name) %>', () => {
4 | test('', async ({ page }) => {
5 |
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/schematics/e2e/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { Tree } from '@angular-devkit/schematics';
2 | import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
3 |
4 | const collectionPath = 'lib/schematics/collection.json';
5 |
6 | describe('e2e', () => {
7 | it('should generate spec file', async () => {
8 | const runner = new SchematicTestRunner('schematics', collectionPath);
9 | const tree = await runner.runSchematic(
10 | 'e2e',
11 | { name: 'hello' },
12 | Tree.empty(),
13 | );
14 |
15 | expect(tree.files).toEqual(['/e2e/hello.spec.ts']);
16 | expect(tree.readContent('/e2e/hello.spec.ts')).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/schematics/e2e/index.ts:
--------------------------------------------------------------------------------
1 | import { strings } from '@angular-devkit/core';
2 | import {
3 | url,
4 | type Rule,
5 | type SchematicContext,
6 | type Tree,
7 | apply,
8 | applyTemplates,
9 | chain,
10 | mergeWith,
11 | move,
12 | } from '@angular-devkit/schematics';
13 |
14 | // You don't have to export the function as default. You can also have more than one rule factory
15 | // per file.
16 | export default function e2e(options: { name: string }): Rule {
17 | return (tree: Tree, _context: SchematicContext) => {
18 | const templateSource = apply(url('./files'), [
19 | applyTemplates({
20 | classify: strings.classify,
21 | name: options.name,
22 | dasherize: strings.dasherize,
23 | }),
24 | move('e2e'),
25 | ]);
26 |
27 | const rule = chain([mergeWith(templateSource)]);
28 |
29 | return rule(tree, _context);
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/schematics/e2e/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "$id": "Playwright",
4 | "title": "Playwright E2E Schema",
5 | "type": "object",
6 | "properties": {
7 | "name": {
8 | "type": "string",
9 | "$default": {
10 | "$source": "argv",
11 | "index": 0
12 | },
13 | "x-prompt": "Name for spec to be created:"
14 | }
15 | },
16 | "required": []
17 | }
18 |
--------------------------------------------------------------------------------
/src/schematics/install-browsers/index.ts:
--------------------------------------------------------------------------------
1 | import { spawnSync } from 'node:child_process';
2 | import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
3 |
4 | export default function installBrowsers(): Rule {
5 | return (tree: Tree, context: SchematicContext) => {
6 | context.logger.info('Installing browsers...');
7 |
8 | spawnSync('npx playwright install', [], {
9 | cwd: process.cwd(),
10 | stdio: 'inherit',
11 | shell: true,
12 | });
13 |
14 | return tree;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/files/e2e/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('has title', async ({ page }) => {
4 | await page.goto('/');
5 |
6 | // Expect a title "to contain" a substring.
7 | await expect(page).toHaveTitle(/MyApp/);
8 | });
9 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/files/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["./**/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/files/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './e2e',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env['CI'],
18 | /* Retry on CI only */
19 | retries: process.env['CI'] ? 2 : 0,
20 | /* Opt out of parallel tests on CI. */
21 | workers: process.env['CI'] ? 1 : undefined,
22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23 | reporter: 'html',
24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25 | use: {
26 | /* Base URL to use in actions like `await page.goto('/')`. */
27 | baseURL: process.env['PLAYWRIGHT_TEST_BASE_URL'] ?? 'http://localhost:4200',
28 |
29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
30 | trace: 'on-first-retry',
31 | },
32 |
33 | /* Configure projects for major browsers */
34 | projects: [
35 | {
36 | name: 'chromium',
37 | use: { ...devices['Desktop Chrome'] },
38 | },
39 |
40 | {
41 | name: 'firefox',
42 | use: { ...devices['Desktop Firefox'] },
43 | },
44 |
45 | {
46 | name: 'webkit',
47 | use: { ...devices['Desktop Safari'] },
48 | },
49 |
50 | /* Test against mobile viewports. */
51 | // {
52 | // name: 'Mobile Chrome',
53 | // use: { ...devices['Pixel 5'] },
54 | // },
55 | // {
56 | // name: 'Mobile Safari',
57 | // use: { ...devices['iPhone 12'] },
58 | // },
59 |
60 | /* Test against branded browsers. */
61 | // {
62 | // name: 'Microsoft Edge',
63 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
64 | // },
65 | // {
66 | // name: 'Google Chrome',
67 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
68 | // },
69 | ],
70 | });
71 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/index.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SchematicTestRunner,
3 | type UnitTestTree,
4 | } from '@angular-devkit/schematics/testing';
5 |
6 | const collectionPath = 'lib/schematics/collection.json';
7 |
8 | describe('ng-add', () => {
9 | const runner = new SchematicTestRunner('schematics', collectionPath);
10 | const npmResponse = jest
11 | .fn()
12 | .mockResolvedValue({ 'dist-tags': { latest: '1.2.3' } });
13 | let appTree: UnitTestTree;
14 |
15 | beforeEach(async () => {
16 | appTree = await runner.runExternalSchematic(
17 | '@schematics/angular',
18 | 'ng-new',
19 | {
20 | name: 'sandbox',
21 | directory: '.',
22 | version: '18.0.0',
23 | },
24 | );
25 | });
26 |
27 | it('should add "e2e" to angular', async () => {
28 | global.fetch = jest.fn().mockResolvedValueOnce({ json: npmResponse });
29 |
30 | const tree = await runner.runSchematic('ng-add', {}, appTree);
31 |
32 | const angularJSON = JSON.parse(tree.readContent('/angular.json'));
33 | expect(angularJSON.projects.sandbox.architect.e2e.builder).toBe(
34 | 'playwright-ng-schematics:playwright',
35 | );
36 | });
37 |
38 | it('should add npm script', async () => {
39 | global.fetch = jest.fn().mockResolvedValueOnce({ json: npmResponse });
40 |
41 | const tree = await runner.runSchematic('ng-add', {}, appTree);
42 |
43 | const packageJSON = JSON.parse(tree.readContent('/package.json'));
44 | expect(packageJSON.scripts.e2e).toBe('ng e2e');
45 | });
46 |
47 | it('should update .gitignore', async () => {
48 | global.fetch = jest.fn().mockResolvedValueOnce({ json: npmResponse });
49 |
50 | const tree = await runner.runSchematic('ng-add', {}, appTree);
51 |
52 | const gitignore = tree.readContent('/.gitignore');
53 | expect(gitignore).toContain('# Playwright');
54 | });
55 |
56 | it('should add files and update devDependencies', async () => {
57 | global.fetch = jest.fn().mockResolvedValueOnce({ json: npmResponse });
58 |
59 | const tree = await runner.runSchematic('ng-add', {}, appTree);
60 |
61 | expect(tree.files).toContain('/playwright.config.ts');
62 | expect(tree.files).toContain('/e2e/tsconfig.json');
63 | expect(tree.files).toContain('/e2e/example.spec.ts');
64 |
65 | const packageJSON = JSON.parse(tree.readContent('/package.json'));
66 | expect(packageJSON.devDependencies['@playwright/test']).toEqual('1.2.3');
67 | // check that the dependency is added in the correct place
68 | expect(Object.keys(packageJSON.devDependencies)).toEqual(
69 | Object.keys(packageJSON.devDependencies).sort(),
70 | );
71 | });
72 |
73 | it(`should install latest if can't fetch version from npm`, async () => {
74 | global.fetch = jest.fn().mockRejectedValueOnce({});
75 |
76 | const tree = await runner.runSchematic('ng-add', {}, appTree);
77 |
78 | const packageJSON = JSON.parse(tree.readContent('/package.json'));
79 | expect(packageJSON.devDependencies['@playwright/test']).toEqual('latest');
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | url,
3 | type Rule,
4 | type SchematicContext,
5 | type Tree,
6 | apply,
7 | chain,
8 | mergeWith,
9 | move,
10 | } from '@angular-devkit/schematics';
11 | import {
12 | NodePackageInstallTask,
13 | RunSchematicTask,
14 | } from '@angular-devkit/schematics/tasks';
15 |
16 | export default function ngAdd(options: { installBrowsers: boolean }): Rule {
17 | return (tree: Tree, context: SchematicContext) => {
18 | const copyFiles = mergeWith(apply(url('./files'), [move('.')]));
19 | const rules = [
20 | updateAngular,
21 | addNpmScript,
22 | gitignore,
23 | copyFiles,
24 | addPlaywright,
25 | ];
26 | if (options.installBrowsers) {
27 | context.addTask(new RunSchematicTask('install-browsers', {}));
28 | }
29 | return chain(rules)(tree, context);
30 | };
31 | }
32 |
33 | function updateAngular(tree: Tree, context: SchematicContext) {
34 | if (!tree.exists('angular.json')) {
35 | return tree;
36 | }
37 | context.logger.debug('angular.json');
38 |
39 | const sourceText = tree.readText('angular.json');
40 | const json = JSON.parse(sourceText);
41 | for (const projectName of Object.keys(json.projects)) {
42 | json.projects[projectName].architect.e2e = {
43 | builder: 'playwright-ng-schematics:playwright',
44 | options: {
45 | devServerTarget: `${projectName}:serve`,
46 | },
47 | configurations: {
48 | production: {
49 | devServerTarget: `${projectName}:serve:production`,
50 | },
51 | },
52 | };
53 | }
54 | tree.overwrite('angular.json', JSON.stringify(json, null, 2));
55 |
56 | return tree;
57 | }
58 |
59 | function addNpmScript(tree: Tree, context: SchematicContext) {
60 | if (!tree.exists('package.json')) {
61 | return tree;
62 | }
63 | context.logger.debug('npm script');
64 |
65 | const key = 'e2e';
66 | const value = 'ng e2e';
67 |
68 | const sourceText = tree.readText('package.json');
69 | const json = JSON.parse(sourceText);
70 | if (!json.scripts[key]) {
71 | json.scripts[key] = value;
72 | }
73 | tree.overwrite('package.json', JSON.stringify(json, null, 2));
74 |
75 | return tree;
76 | }
77 |
78 | function gitignore(tree: Tree, context: SchematicContext) {
79 | if (!tree.exists('.gitignore')) {
80 | return tree;
81 | }
82 | context.logger.debug('Adjust .gitignore');
83 |
84 | const content = tree.readText('.gitignore');
85 | const modifiedContent = `${content}
86 | # Playwright
87 | /test-results/
88 | /playwright-report/
89 | /playwright/.cache/
90 | `;
91 | tree.overwrite('.gitignore', modifiedContent);
92 |
93 | return tree;
94 | }
95 |
96 | async function getLatestNpmVersion(packageName: string) {
97 | try {
98 | const response = await fetch(`https://registry.npmjs.org/${packageName}`);
99 | const responseObject = await response.json();
100 | const version = responseObject['dist-tags'].latest ?? 'latest';
101 | return version;
102 | } catch (error) {
103 | return 'latest';
104 | }
105 | }
106 |
107 | function addPackageToPackageJson(
108 | tree: Tree,
109 | context: SchematicContext,
110 | pkg: string,
111 | version: string,
112 | ): Rule {
113 | return () => {
114 | if (!tree.exists('package.json')) {
115 | return tree;
116 | }
117 | context.logger.debug('Adjust package.json');
118 |
119 | const sourceText = tree.readText('package.json');
120 | const json = JSON.parse(sourceText);
121 | if (!json.devDependencies) {
122 | json.devDependencies = {};
123 | }
124 | if (!json.devDependencies[pkg]) {
125 | json.devDependencies[pkg] = version;
126 | }
127 | json.devDependencies = sortObjectByKeys(json.devDependencies);
128 | tree.overwrite('package.json', JSON.stringify(json, null, 2));
129 |
130 | return tree;
131 | };
132 | }
133 |
134 | async function addPlaywright(tree: Tree, context: SchematicContext) {
135 | context.logger.debug('Updating dependencies...');
136 | const version = await getLatestNpmVersion('@playwright/test');
137 |
138 | context.logger.info(`Adding @playwright/test ${version}`);
139 |
140 | context.addTask(new NodePackageInstallTask({ allowScripts: true }));
141 |
142 | return addPackageToPackageJson(tree, context, '@playwright/test', version);
143 | }
144 |
145 | function sortObjectByKeys(
146 | obj: Record,
147 | ): Record {
148 | return Object.keys(obj)
149 | .sort()
150 | .reduce((result, key) => {
151 | return {
152 | // biome-ignore lint/performance/noAccumulatingSpread: small object, no perf cost
153 | ...result,
154 | [key]: obj[key],
155 | };
156 | }, {});
157 | }
158 |
--------------------------------------------------------------------------------
/src/schematics/ng-add/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "$id": "Playwright",
4 | "title": "Playwright ng-add Schema",
5 | "type": "object",
6 | "properties": {
7 | "installBrowsers": {
8 | "type": "boolean",
9 | "default": false,
10 | "x-prompt": "Install Playwright browsers (can be done manually via 'npx playwright install')?"
11 | }
12 | },
13 | "required": []
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "tsconfig",
4 | "outDir": "lib/",
5 | "lib": ["es2018", "dom"],
6 | "declaration": true,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "noEmitOnError": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "noImplicitAny": true,
12 | "noImplicitThis": true,
13 | "noUnusedParameters": false,
14 | "noUnusedLocals": false,
15 | "rootDir": "src/",
16 | "skipDefaultLibCheck": true,
17 | "skipLibCheck": true,
18 | "sourceMap": false,
19 | "strictNullChecks": true,
20 | "target": "es6",
21 | "types": ["node", "jest"]
22 | },
23 | "include": ["src/**/*"],
24 | "exclude": ["src/schematics/*/files/**/*", "src/**/*spec.ts"]
25 | }
26 |
--------------------------------------------------------------------------------