frontend-go-live-check 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +113 -23
- package/package.json +1 -1
- package/schema.json +2 -2
- package/src/api/model.mjs +1 -1
- package/src/checks/auth.mjs +1 -1
- package/src/checks/browser.mjs +5 -1
- package/src/checks/code.mjs +1 -1
- package/src/checks/sso.mjs +1 -1
- package/src/checks/visual.mjs +7 -2
- package/src/cli/main.mjs +5 -5
- package/src/commands/ci.mjs +18 -18
- package/src/commands/doctor.mjs +9 -8
- package/src/commands/init.mjs +34 -27
- package/src/commands/login.mjs +14 -11
- package/src/commands/run.mjs +9 -9
- package/src/core/config.mjs +48 -22
- package/src/core/explain.mjs +4 -2
- package/src/core/login.mjs +1 -1
- package/src/core/project.mjs +1 -1
- package/src/core/routes.mjs +1 -1
- package/src/core/rules.mjs +1 -1
- package/src/core/session.mjs +1 -1
- package/src/export/docx.mjs +5 -4
- package/src/export/model.mjs +60 -16
- package/src/export/pdf.mjs +5 -4
- package/src/report/report.mjs +99 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
- **One folder for everything**: `go-live-check/` holds the settings (`config.json`), the test passwords (`.env`), the API files (`api/`), the design baseline (`baseline/`), the report (`report/`) and a saved sign-in (`session.json`), with its own `.gitignore`. `init` creates it. Projects with the older root files keep working; the README shows how to move.
|
|
6
|
+
- Every section of the HTML report opens and closes (accordion), with **Expand all** and **Collapse all**.
|
|
7
|
+
- The date and time the report was created are at the top of the HTML report, on the PDF and Word cover, and in the page header.
|
|
8
|
+
- Visual changes show the real pages: **Before** (the approved look), **Now** and **Changes** side by side; in each finding, Actual is the page now and Expected is the approved look.
|
|
9
|
+
|
|
10
|
+
## 1.4.1
|
|
11
|
+
|
|
12
|
+
- Problems are grouped **by type** (Functionality, Design, Accessibility, Performance, Sign-in, APIs, Security, SEO, Code) in the HTML report, the PDF and the Word file, most serious first in each group.
|
|
13
|
+
- Every problem has **Steps to reproduce** and **Test again**: the shortest command that checks only that problem after the fix.
|
|
14
|
+
- README: new *How it works* and *Built with* sections.
|
|
15
|
+
|
|
3
16
|
## 1.4.0
|
|
4
17
|
|
|
5
18
|
- New layout checks on every page at every screen size, each with Actual and Expected pictures where possible:
|
package/README.md
CHANGED
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
20
|
<a href="#quick-start">Quick start</a> ·
|
|
21
|
+
<a href="#files-everything-in-one-folder">Files</a> ·
|
|
21
22
|
<a href="#sign-in-email-google-microsoft-uae-pass">Sign-in</a> ·
|
|
22
23
|
<a href="#what-it-checks">What it checks</a> ·
|
|
24
|
+
<a href="#how-it-works">How it works</a> ·
|
|
23
25
|
<a href="#the-report">The report</a> ·
|
|
24
26
|
<a href="#pipelines-approve-or-reject-the-merge">Pipelines</a> ·
|
|
25
27
|
<a href="#settings">Settings</a> ·
|
|
28
|
+
<a href="#built-with">Built with</a> ·
|
|
26
29
|
<a href="#faq">FAQ</a>
|
|
27
30
|
</p>
|
|
28
31
|
|
|
@@ -36,11 +39,11 @@ npx frontend-go-live-check init
|
|
|
36
39
|
npm run go-live
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
That's it. The result is `go-live-report/index.html`, plus a PDF and a Word file to share with your team.
|
|
42
|
+
That's it. The result is `go-live-check/report/index.html`, plus a PDF and a Word file to share with your team.
|
|
40
43
|
|
|
41
44
|
`init` adds the `go-live` scripts and a settings file, and downloads the test browser. It never overwrites anything.
|
|
42
45
|
|
|
43
|
-
Have an API? Save your Postman collection as **`api/postman_collection.json`** (or your OpenAPI file as `api/openapi.json`) and the API tests run too.
|
|
46
|
+
Have an API? Save your Postman collection as **`go-live-check/api/postman_collection.json`** (or your OpenAPI file as `go-live-check/api/openapi.json`) and the API tests run too.
|
|
44
47
|
|
|
45
48
|
<p align="center">
|
|
46
49
|
<img src="https://unpkg.com/frontend-go-live-check@1/docs/terminal.png" alt="Terminal output: NOT READY, 2 blocker, 7 major, 5 minor, with a score per area" width="760">
|
|
@@ -48,6 +51,53 @@ Have an API? Save your Postman collection as **`api/postman_collection.json`** (
|
|
|
48
51
|
|
|
49
52
|
---
|
|
50
53
|
|
|
54
|
+
## Files: everything in one folder
|
|
55
|
+
|
|
56
|
+
Everything the tool uses or makes is in **one `go-live-check/` folder** at the root of your project. `init` creates it.
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
my-project/
|
|
60
|
+
├── go-live-check/
|
|
61
|
+
│ ├── config.json ← the settings (all optional)
|
|
62
|
+
│ ├── .env ← test passwords (Git ignores it)
|
|
63
|
+
│ ├── .env.example ← the names to fill in (committed)
|
|
64
|
+
│ ├── api/
|
|
65
|
+
│ │ ├── postman_collection.json ← your Postman collection
|
|
66
|
+
│ │ ├── postman_environment.json ← optional: its environment
|
|
67
|
+
│ │ └── openapi.json ← or/and your OpenAPI file
|
|
68
|
+
│ ├── baseline/ ← the approved look of every page (commit it)
|
|
69
|
+
│ ├── report/ ← the results: HTML, PDF, Word, zip (Git ignores it)
|
|
70
|
+
│ ├── session.json ← a saved sign-in, after `login` (Git ignores it)
|
|
71
|
+
│ └── .gitignore ← keeps .env, report/ and session.json out of Git
|
|
72
|
+
├── src/
|
|
73
|
+
└── package.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Commit | Never commit (already ignored) |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `config.json`, `.env.example`, `api/`, `baseline/` | `.env`, `report/`, `session.json` |
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary><b>Set up with an older version?</b></summary>
|
|
82
|
+
|
|
83
|
+
Projects with `go-live-check.config.json`, `go-live-check.env`, `api/`, `go-live-baseline/` and `go-live-report/` at the root keep working. To move to the folder:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mkdir -p go-live-check
|
|
87
|
+
git mv go-live-check.config.json go-live-check/config.json
|
|
88
|
+
git mv go-live-check.env.example go-live-check/.env.example
|
|
89
|
+
git mv api go-live-check/api
|
|
90
|
+
git mv go-live-baseline go-live-check/baseline
|
|
91
|
+
mv go-live-check.env go-live-check/.env
|
|
92
|
+
printf '.env\nreport/\nsession.json\n' > go-live-check/.gitignore
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
In `go-live-check/config.json`, change `$schema` to `../node_modules/frontend-go-live-check/schema.json`. Remove the old `go-live-report/` folder and its `.gitignore` lines.
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
51
101
|
## Why
|
|
52
102
|
|
|
53
103
|
| Without it | With it |
|
|
@@ -97,7 +147,7 @@ npx frontend-go-live-check login
|
|
|
97
147
|
What happens:
|
|
98
148
|
|
|
99
149
|
- A normal browser window opens on your app. Sign in the way your users do (approve UAE PASS on your phone), wait for a signed-in page, then press Enter in the terminal.
|
|
100
|
-
- The session (cookies and browser storage) is saved in **`go-live-check
|
|
150
|
+
- The session (cookies and browser storage) is saved in **`go-live-check/session.json`**. Treat it like a password: Git ignores it, never share it.
|
|
101
151
|
- When it expires, the report says ***"The saved sign-in session has expired"*** (also when the app's API starts answering 401), and `doctor` tells you before you run. Run `login` again.
|
|
102
152
|
|
|
103
153
|
**Pipelines (`ciLogin`)**: a pipeline has no one to approve a sign-in, so it uses `ciLogin` instead of `login`. Pick one of these:
|
|
@@ -145,7 +195,7 @@ The report lists every sign-in method found (*email and password · Google · UA
|
|
|
145
195
|
| 🧭 | **Functionality** | JavaScript errors, console errors, failed requests, broken links, images and anchors, blank pages, buttons covered by something else, unfinished text (`undefined`, `NaN`, `[object Object]`, translation keys, lorem ipsum, TODO), loading spinners that never stop, the production build, the "page not found" page |
|
|
146
196
|
| 🛟 | **Resilience** | Each page opened again with its API calls failing: does it crash, go blank, or tell the user? |
|
|
147
197
|
| 📱 | **Design and responsive layout** | Every page at every screen size: sideways scrolling, text that spills out of its box (a button too small for its label), text on top of other text, content cut off at the screen edge, text cut off inside its box, overlapping buttons, stretched or squashed images, tiny text on phones, right-to-left layout (Arabic, Hebrew…), fonts that fail to load, too many fonts and sizes |
|
|
148
|
-
| 🖼️ | **Visual changes** |
|
|
198
|
+
| 🖼️ | **Visual changes** | Every page compared with its approved look (the baseline): **Before**, **Now** and the changed areas side by side |
|
|
149
199
|
| ♿ | **Accessibility** | axe-core against WCAG 2.0 / 2.1 / 2.2 A and AA; keyboard-only use: visible focus, no traps, nothing that only a mouse can click; form field types |
|
|
150
200
|
| ⚡ | **Performance** | Size, JavaScript, requests, load time, LCP, layout shift and frozen-page time for every page, against budgets; Lighthouse as a phone and as a desktop |
|
|
151
201
|
| 🔌 | **API calls made by the pages** | Slow, very large, repeated, too many, uncompressed or uncached calls |
|
|
@@ -160,6 +210,24 @@ The report's **What was checked** table says exactly what ran on your project, a
|
|
|
160
210
|
|
|
161
211
|
---
|
|
162
212
|
|
|
213
|
+
## How it works
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
① Detect ──▶ ② Build & start ──▶ ③ Sign in ──▶ ④ Find pages ──▶ ⑤ Test ──▶ ⑥ Report ──▶ ⑦ Verdict
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Step | What happens |
|
|
220
|
+
| :-: | --- |
|
|
221
|
+
| **1. Detect** | Reads `package.json`: the framework, the package manager, the sign-in libraries, and your files in `go-live-check/` |
|
|
222
|
+
| **2. Build & start** | Builds the app and starts it **the way it runs in production**, plus any services you listed (a backend or a mock API) |
|
|
223
|
+
| **3. Sign in** | With the test account, the saved session (`login`) or a test token, so the signed-in pages are tested too |
|
|
224
|
+
| **4. Find pages** | Follows every link, reads your routes in the source code and your `sitemap.xml` |
|
|
225
|
+
| **5. Test** | Opens every page at every screen size in a real browser and runs every check in [What it checks](#what-it-checks); then calls every API endpoint from your Postman or OpenAPI file |
|
|
226
|
+
| **6. Report** | Writes the HTML report with Actual and Expected pictures, plus the PDF, the Word file and the zip |
|
|
227
|
+
| **7. Verdict** | **READY**, **NEEDS ATTENTION** or **NOT READY**; in a pipeline, the run fails on blockers and the merge is rejected |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
163
231
|
## Works with
|
|
164
232
|
|
|
165
233
|
**Next.js** · **React** (Vite, Create React App) · **Vue** · **Nuxt** · **Angular** · **Svelte / SvelteKit** · **Astro** · **Remix / React Router** · **Gatsby** · **Solid** · **Preact** · **Qwik** · **plain HTML**. Anything else, in any language, works with a start command or `--url`.
|
|
@@ -194,10 +262,11 @@ The framework and the package manager (npm, pnpm, yarn, bun) are detected. The a
|
|
|
194
262
|
|
|
195
263
|
The report has:
|
|
196
264
|
|
|
197
|
-
- the **verdict** and a **score per area** (click an area to
|
|
198
|
-
- every
|
|
265
|
+
- when it was created, the **verdict** and a **score per area** (click an area to jump to it);
|
|
266
|
+
- every section opens and closes, with **Expand all** and **Collapse all**;
|
|
267
|
+
- **problems by type**: Functionality, Design, Accessibility, Performance, Sign-in, APIs, Security, SEO, Code, each with its score, worst problems first;
|
|
199
268
|
- the API results, the speed of every page, and the load test;
|
|
200
|
-
- screenshots of every page at every size.
|
|
269
|
+
- screenshots of every page at every size, for the design review.
|
|
201
270
|
|
|
202
271
|
### Every problem: Actual next to Expected
|
|
203
272
|
|
|
@@ -206,7 +275,9 @@ Each problem is shown so a developer understands it at a glance, without knowing
|
|
|
206
275
|
- **one sentence** on what is wrong, in plain words;
|
|
207
276
|
- **Actual**: a close-up of the page as it is now, with the problem outlined in red, at the screen size where it happens;
|
|
208
277
|
- **Expected**: the same spot as it should be, outlined in green. For visual problems the tool corrects the page in the browser for this one picture: a visible focus ring, readable contrast, text that fits, a page that fits the screen, right-to-left layout. Other problems (APIs, security, sign-in) get one sentence saying what is expected;
|
|
209
|
-
- **where** (page, screen size, the element by name and CSS selector)
|
|
278
|
+
- **where** (page, screen size, the element by name and CSS selector) and **who it affects**;
|
|
279
|
+
- **steps to reproduce** it yourself;
|
|
280
|
+
- **test again**: the shortest command that checks only this problem after the fix (copy button included), for example `npx frontend-go-live-check --only pages --pages /login --max-pages 1`.
|
|
210
281
|
|
|
211
282
|
<p align="center">
|
|
212
283
|
<img src="https://unpkg.com/frontend-go-live-check@1/docs/finding.png" alt="A problem card: Actual (focus not visible, outlined in red) next to Expected (a clear focus ring, outlined in green)" width="860">
|
|
@@ -314,18 +385,18 @@ The short name `go-live-check` works too. After `init`, use `npm run go-live`, `
|
|
|
314
385
|
|
|
315
386
|
## Settings
|
|
316
387
|
|
|
317
|
-
Everything is optional. `init` writes `go-live-check
|
|
388
|
+
Everything is optional. `init` writes `go-live-check/config.json`, and your editor autocompletes it through `$schema`.
|
|
318
389
|
|
|
319
390
|
```json
|
|
320
391
|
{
|
|
321
|
-
"$schema": "
|
|
392
|
+
"$schema": "../node_modules/frontend-go-live-check/schema.json",
|
|
322
393
|
"login": { "page": "/login", "username": "test.user", "password": "$GO_LIVE_PASSWORD" },
|
|
323
394
|
"services": [{ "command": "npm run api", "url": "http://localhost:4000/health" }],
|
|
324
395
|
"failOn": "blocker"
|
|
325
396
|
}
|
|
326
397
|
```
|
|
327
398
|
|
|
328
|
-
API tests need no settings when your files are in `api/` (
|
|
399
|
+
API tests need no settings when your files are in `go-live-check/api/` (see *API tests from Postman or OpenAPI* below). Passwords are written as `"$NAME"`. Put their values in `go-live-check/.env` (Git ignores it) or in your pipeline's secrets. They are replaced with `‹secret›` in every report.
|
|
329
400
|
|
|
330
401
|
<details>
|
|
331
402
|
<summary><b>🔐 Sign-in forms that need help</b></summary>
|
|
@@ -354,19 +425,16 @@ An external single sign-on page usually can't be automated. Point `login` at a t
|
|
|
354
425
|
<details>
|
|
355
426
|
<summary><b>🧪 API tests from Postman or OpenAPI</b></summary>
|
|
356
427
|
|
|
357
|
-
Put your API description in
|
|
428
|
+
Put your API description in **`go-live-check/api/`**, with **exactly these names**:
|
|
358
429
|
|
|
359
430
|
```text
|
|
360
|
-
|
|
361
|
-
├──
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
│ └── openapi.json ← or/and the OpenAPI / Swagger file (openapi.yaml works too)
|
|
365
|
-
├── go-live-check.config.json
|
|
366
|
-
└── package.json
|
|
431
|
+
go-live-check/api/
|
|
432
|
+
├── postman_collection.json ← the Postman collection (v2.1)
|
|
433
|
+
├── postman_environment.json ← optional: its environment ({{baseUrl}} and other variables)
|
|
434
|
+
└── openapi.json ← or/and the OpenAPI / Swagger file (openapi.yaml works too)
|
|
367
435
|
```
|
|
368
436
|
|
|
369
|
-
That's all: no settings are needed. Postman export names (`My API.postman_collection.json`) need renaming to `postman_collection.json`. If both a Postman collection and an OpenAPI file are there, they are combined: OpenAPI gives the rules, Postman the examples. `init` creates `api/README.md` with these instructions. `frontend-go-live-check doctor` shows what it found.
|
|
437
|
+
That's all: no settings are needed. Postman export names (`My API.postman_collection.json`) need renaming to `postman_collection.json`. If both a Postman collection and an OpenAPI file are there, they are combined: OpenAPI gives the rules, Postman the examples. `init` creates `go-live-check/api/README.md` with these instructions. `frontend-go-live-check doctor` shows what it found.
|
|
370
438
|
|
|
371
439
|
Never put real passwords or tokens in these files: use `$NAMES` (see below).
|
|
372
440
|
|
|
@@ -394,7 +462,7 @@ To test roles and ownership, add test users. Include a **second user of the same
|
|
|
394
462
|
<details>
|
|
395
463
|
<summary><b>🖼️ Visual changes</b></summary>
|
|
396
464
|
|
|
397
|
-
The first run saves a picture of every page at every size in `go-live-baseline/`. Commit that folder. Later runs report pages that look different, with the changed areas in red.
|
|
465
|
+
The first run saves a picture of every page at every size in `go-live-check/baseline/`. Commit that folder. Later runs report pages that look different, with the changed areas in red.
|
|
398
466
|
|
|
399
467
|
After an intended design change, save the new look:
|
|
400
468
|
|
|
@@ -433,7 +501,7 @@ Hide parts that change on their own (clocks, live data) with `"visualMask": [".c
|
|
|
433
501
|
## Safety and privacy
|
|
434
502
|
|
|
435
503
|
- **Everything runs on your machine** or your own pipeline. Nothing is sent anywhere except to the app being tested.
|
|
436
|
-
- **Test passwords** come from `go-live-check
|
|
504
|
+
- **Test passwords** come from `go-live-check/.env` or pipeline secrets, and never appear in a report or log.
|
|
437
505
|
- **Use test accounts on a test environment.** Browser checks only read pages and submit forms empty.
|
|
438
506
|
- **API writes and the load test** run only against this computer or the hosts you list. Every record the tests create is deleted afterwards.
|
|
439
507
|
- **Closed networks.** Point `PLAYWRIGHT_DOWNLOAD_HOST` at your mirror, or `GO_LIVE_CHROMIUM` at an existing Chromium.
|
|
@@ -451,6 +519,28 @@ console.log(summary.verdict); // READY · NEEDS ATTENTION · NOT READY
|
|
|
451
519
|
|
|
452
520
|
---
|
|
453
521
|
|
|
522
|
+
## Built with
|
|
523
|
+
|
|
524
|
+
Trusted open-source tools, used by thousands of teams, do the heavy lifting. The checks, the sign-in handling, the layout and content checks and the reports are this package's own code on top of them.
|
|
525
|
+
|
|
526
|
+
| Job | Tool | What it does here |
|
|
527
|
+
| --- | --- | --- |
|
|
528
|
+
| 🌐 **Real browser** | [Playwright](https://playwright.dev) | Opens every page in Chromium (Firefox and WebKit with `--all-browsers`) at every screen size; clicks, types, uses the keyboard, signs in, takes the screenshots and prints the PDF |
|
|
529
|
+
| ♿ **Accessibility** | [axe-core](https://github.com/dequelabs/axe-core) (`@axe-core/playwright`) | The accessibility rules (WCAG 2.0 / 2.1 / 2.2, A and AA) used by most accessibility tools |
|
|
530
|
+
| ⚡ **Speed and quality** | [Lighthouse](https://developer.chrome.com/docs/lighthouse) | Google's performance, best-practice and SEO scores, as a phone and as a desktop |
|
|
531
|
+
| | [chrome-launcher](https://github.com/GoogleChrome/chrome-launcher) · [puppeteer-core](https://pptr.dev) | Start and control Chrome for Lighthouse, including signed-in pages |
|
|
532
|
+
| 🖼️ **Visual changes** | [pixelmatch](https://github.com/mapbox/pixelmatch) · [pngjs](https://github.com/pngjs/pngjs) | Compare every screenshot with the saved baseline, pixel by pixel, and mark the changes in red |
|
|
533
|
+
| 🧪 **API tests** | [Ajv](https://ajv.js.org) · [ajv-formats](https://github.com/ajv-validator/ajv-formats) | Check every API answer against your OpenAPI or Postman schema, including formats (email, date, URL, ID) |
|
|
534
|
+
| | [Faker](https://fakerjs.dev) | Realistic test data (names, emails, numbers) for the requests that create records |
|
|
535
|
+
| | [yaml](https://eemeli.org/yaml) | Reads OpenAPI files written in YAML |
|
|
536
|
+
| 📄 **Documents** | [docx](https://docx.js.org) | Writes the Word report |
|
|
537
|
+
| | [JSZip](https://stuk.github.io/jszip) | Packs the full report, screenshots, PDF and Word file into one zip |
|
|
538
|
+
| 🛡️ **Dependencies** | `npm audit` (comes with npm) | Known security problems in your project's libraries |
|
|
539
|
+
|
|
540
|
+
No other services: nothing is uploaded, and no account is needed.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
454
544
|
## FAQ
|
|
455
545
|
|
|
456
546
|
<details>
|
|
@@ -490,7 +580,7 @@ Security headers, compression and caching usually come from your web server or C
|
|
|
490
580
|
| You see | Fix |
|
|
491
581
|
| --- | --- |
|
|
492
582
|
| `Executable doesn't exist` | `npx playwright install chromium` |
|
|
493
|
-
| `Not set: GO_LIVE_PASSWORD` | Copy `go-live-check
|
|
583
|
+
| `Not set: GO_LIVE_PASSWORD` | Copy `go-live-check/.env.example` to `go-live-check/.env` and fill it in |
|
|
494
584
|
| `login-failed` | Check `login`; name the fields (`usernameField`, `passwordField`, `submit`) if the form is unusual |
|
|
495
585
|
| *Signed-in pages were not tested* | Set `login`, or run `npx frontend-go-live-check login` (Google, UAE PASS…) |
|
|
496
586
|
| *The saved sign-in session has expired* | Run `npx frontend-go-live-check login` again |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontend-go-live-check",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Is your frontend ready to go live? One command tests any web frontend (React, Next.js, Vue, Angular, Svelte, plain HTML…): pages, responsive design, accessibility, keyboard, performance and load, API calls, security, SEO, sign-in and APIs from Postman/OpenAPI. Report with evidence, PDF and Word, and a pipeline gate that approves or rejects the merge.",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/frontend-go-live-check",
|
|
6
6
|
"repository": {
|
package/schema.json
CHANGED
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
},
|
|
155
155
|
"password": {
|
|
156
156
|
"type": "string",
|
|
157
|
-
"description": "Use \"$NAME\" and put NAME in go-live-check
|
|
157
|
+
"description": "Use \"$NAME\" and put NAME in go-live-check/.env."
|
|
158
158
|
},
|
|
159
159
|
"usernameField": {
|
|
160
160
|
"type": "string",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
},
|
|
208
208
|
"sessionFile": {
|
|
209
209
|
"type": "string",
|
|
210
|
-
"description": "Where the saved session is kept. Default go-live-check
|
|
210
|
+
"description": "Where the saved session is kept. Default go-live-check/session.json (never commit it)."
|
|
211
211
|
}
|
|
212
212
|
},
|
|
213
213
|
"description": "How the tool signs in. Email/password: page + username + password. Google, Microsoft, Apple, UAE PASS\u2026: \"method\": \"session\" and run frontend-go-live-check login once. Pipelines: see ciLogin."
|
package/src/api/model.mjs
CHANGED
|
@@ -242,6 +242,6 @@ export async function loadApiMap(api, dir) {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
const baseUrl = sources.find((s) => s.baseUrl)?.baseUrl;
|
|
245
|
-
if (!sources.length) notes.push('No API file given: set "api": { "file": "postman_collection.json" } (or "openapi") in go-live-check
|
|
245
|
+
if (!sources.length) notes.push('No API file given: set "api": { "file": "postman_collection.json" } (or "openapi") in go-live-check/config.json, or put the file in go-live-check/api/, or --api <file>.');
|
|
246
246
|
return { endpoints: [...map.values()], baseUrl, basePath: sources.find((s) => s.basePath)?.basePath, title: sources.find((s) => s.title)?.title, sources: sources.map((s) => s.kind), notes };
|
|
247
247
|
}
|
package/src/checks/auth.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export async function runAuthChecks({ base, config, pages, network, log }) {
|
|
|
95
95
|
const landed = new URL(page.url()).pathname;
|
|
96
96
|
const asksToSignIn = (await page.locator('input[type=password]').count()) > 0;
|
|
97
97
|
if (landed === new URL(target.url).pathname && !asksToSignIn) {
|
|
98
|
-
add({ ruleId: 'page-without-sign-in', page: target.key, detail: `${target.key} opened without signing in. If it is public on purpose, add it to "publicPages" in go-live-check
|
|
98
|
+
add({ ruleId: 'page-without-sign-in', page: target.key, detail: `${target.key} opened without signing in. If it is public on purpose, add it to "publicPages" in the settings (go-live-check/config.json).` });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
await context.close();
|
package/src/checks/browser.mjs
CHANGED
|
@@ -414,7 +414,7 @@ export async function runBrowserChecks({ base, config, reportDir, log, starts =
|
|
|
414
414
|
mkdirSync(join(reportDir, 'visual', 'now'), { recursive: true });
|
|
415
415
|
const taken = await page.screenshot({ path: shotPath, fullPage: true, type: 'png', animations: 'disabled', caret: 'hide', mask }).then(() => true).catch(() => false);
|
|
416
416
|
if (taken) {
|
|
417
|
-
const result = compareWithBaseline({ shotPath, baselineDir, diffDir: join(reportDir, 'visual', 'diff'), name, update: updateBaseline, threshold: config.visualThreshold ?? 0.001 });
|
|
417
|
+
const result = compareWithBaseline({ shotPath, baselineDir, diffDir: join(reportDir, 'visual', 'diff'), beforeDir: join(reportDir, 'visual', 'before'), name, update: updateBaseline, threshold: config.visualThreshold ?? 0.001 });
|
|
418
418
|
visualResults.push({ page: landedKey, view: tag, name, ...result });
|
|
419
419
|
if (result.status === 'changed') {
|
|
420
420
|
add({
|
|
@@ -423,6 +423,10 @@ export async function runBrowserChecks({ base, config, reportDir, log, starts =
|
|
|
423
423
|
view: tag,
|
|
424
424
|
detail: `${(result.ratio * 100).toFixed(1)}% of the page looks different from the saved baseline${result.sizeChanged ? ` (size ${result.before} → ${result.after})` : ''}.`,
|
|
425
425
|
link: `visual/diff/${name}`,
|
|
426
|
+
// Actual = the page now; Expected = the approved look (the baseline).
|
|
427
|
+
evidence: `visual/now/${name}`,
|
|
428
|
+
expected: `visual/before/${name}`,
|
|
429
|
+
evidenceView: tag,
|
|
426
430
|
});
|
|
427
431
|
}
|
|
428
432
|
}
|
package/src/checks/code.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { SECRET_PATTERNS } from './security.mjs';
|
|
|
10
10
|
function sourceFiles(dir, out = [], depth = 0) {
|
|
11
11
|
if (depth > 8 || out.length > 3000) return out;
|
|
12
12
|
for (const name of readdirSync(dir)) {
|
|
13
|
-
if (['node_modules', 'dist', 'build', '.next', 'out', 'coverage', 'reports', 'go-live-report', '.git', 'test-results'].includes(name)) continue;
|
|
13
|
+
if (['node_modules', 'dist', 'build', '.next', 'out', 'coverage', 'reports', 'go-live-report', 'go-live-check', '.git', 'test-results'].includes(name)) continue;
|
|
14
14
|
const path = join(dir, name);
|
|
15
15
|
const stat = statSync(path);
|
|
16
16
|
if (stat.isDirectory()) sourceFiles(path, out, depth + 1);
|
package/src/checks/sso.mjs
CHANGED
|
@@ -124,7 +124,7 @@ export function scanProject(dir) {
|
|
|
124
124
|
continue;
|
|
125
125
|
}
|
|
126
126
|
for (const name of names) {
|
|
127
|
-
if (['node_modules', 'dist', 'build', '.next', '.git', 'go-live-report', 'coverage'].includes(name)) continue;
|
|
127
|
+
if (['node_modules', 'dist', 'build', '.next', '.git', 'go-live-report', 'go-live-check', 'coverage'].includes(name)) continue;
|
|
128
128
|
const path = join(current, name);
|
|
129
129
|
const stat = statSync(path);
|
|
130
130
|
if (stat.isDirectory()) stack.push(path);
|
package/src/checks/visual.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Design changes between runs. The first run saves a picture of every page at every screen size
|
|
3
|
-
* in go-live-baseline/ (commit it). Later runs compare with it and report pages that look
|
|
3
|
+
* in go-live-check/baseline/ (commit it). Later runs compare with it and report pages that look
|
|
4
4
|
* different, with a picture that shows the changed area in red.
|
|
5
5
|
* After an intended design change: frontend-go-live-check --update-baseline.
|
|
6
6
|
*/
|
|
@@ -9,7 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import pixelmatch from 'pixelmatch';
|
|
10
10
|
import { PNG } from 'pngjs';
|
|
11
11
|
|
|
12
|
-
export function compareWithBaseline({ shotPath, baselineDir, diffDir, name, update, threshold = 0.001 }) {
|
|
12
|
+
export function compareWithBaseline({ shotPath, baselineDir, diffDir, beforeDir, name, update, threshold = 0.001 }) {
|
|
13
13
|
mkdirSync(baselineDir, { recursive: true });
|
|
14
14
|
const baselinePath = join(baselineDir, name);
|
|
15
15
|
if (update || !existsSync(baselinePath)) {
|
|
@@ -33,5 +33,10 @@ export function compareWithBaseline({ shotPath, baselineDir, diffDir, name, upda
|
|
|
33
33
|
if (ratio <= threshold && !sizeChanged) return { status: 'same', ratio };
|
|
34
34
|
mkdirSync(diffDir, { recursive: true });
|
|
35
35
|
writeFileSync(join(diffDir, name), PNG.sync.write(diff));
|
|
36
|
+
// The approved look, next to the report, so Before and Now can be shown side by side.
|
|
37
|
+
if (beforeDir) {
|
|
38
|
+
mkdirSync(beforeDir, { recursive: true });
|
|
39
|
+
copyFileSync(baselinePath, join(beforeDir, name));
|
|
40
|
+
}
|
|
36
41
|
return { status: 'changed', ratio, sizeChanged, before: `${a.width}×${a.height}`, after: `${b.width}×${b.height}` };
|
|
37
42
|
}
|
package/src/cli/main.mjs
CHANGED
|
@@ -29,16 +29,16 @@ What to check
|
|
|
29
29
|
--update-baseline save the current look of every page as the new design baseline
|
|
30
30
|
|
|
31
31
|
Output
|
|
32
|
-
--out <folder> where the report goes (default go-live-report)
|
|
32
|
+
--out <folder> where the report goes (default go-live-check/report)
|
|
33
33
|
--open open the report when done
|
|
34
34
|
--fail-on <level> blocker (default) | major | minor | never: what makes the command fail
|
|
35
|
-
--config <file> the settings file (default go-live-check
|
|
35
|
+
--config <file> the settings file (default go-live-check/config.json)
|
|
36
36
|
--quiet print only the result
|
|
37
37
|
--ci pipeline mode: build summary, no prompts (the pipeline files use it)
|
|
38
38
|
--format pdf,docx which documents to write (default both)
|
|
39
39
|
--no-export no Word or PDF document
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Files: go-live-check/ (config.json settings, .env test passwords never committed, api/ Postman or OpenAPI, report/).
|
|
42
42
|
Docs: https://www.npmjs.com/package/go-live-check
|
|
43
43
|
`;
|
|
44
44
|
|
|
@@ -64,7 +64,7 @@ export function parseArgs(argv) {
|
|
|
64
64
|
return { options, positional };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const isProject = (folder) => ['package.json', 'go-live-check.config.json', 'index.html'].some((f) => existsSync(join(folder, f)));
|
|
67
|
+
const isProject = (folder) => ['package.json', 'go-live-check/config.json', 'go-live-check.config.json', 'index.html'].some((f) => existsSync(join(folder, f)));
|
|
68
68
|
|
|
69
69
|
export async function main(argv) {
|
|
70
70
|
let parsed;
|
|
@@ -148,7 +148,7 @@ export async function main(argv) {
|
|
|
148
148
|
|
|
149
149
|
const projectFolder = isProject(folder) ? folder : undefined;
|
|
150
150
|
if (!projectFolder && !options.url) {
|
|
151
|
-
console.error(`✖ ${folder} is not a web project (no package.json, index.html or go-live-check
|
|
151
|
+
console.error(`✖ ${folder} is not a web project (no package.json, index.html or go-live-check/config.json).\n Give a project folder, or --url <address> to check a site that is already running.`);
|
|
152
152
|
return 2;
|
|
153
153
|
}
|
|
154
154
|
const { runCheck } = await import('../commands/run.mjs');
|
package/src/commands/ci.mjs
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
* Writes a pipeline file that runs the full check on every pull / merge request. The run fails
|
|
4
4
|
* (and the merge is blocked once the check is required) when the verdict reaches "failOn".
|
|
5
5
|
* The test passwords come from the pipeline's secret variables: the file lists the names found
|
|
6
|
-
* in
|
|
6
|
+
* in the settings ("$NAME").
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { layout } from '../core/config.mjs';
|
|
11
11
|
|
|
12
12
|
const PLAYWRIGHT = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).dependencies.playwright;
|
|
13
13
|
const IMAGE = `mcr.microsoft.com/playwright:v${PLAYWRIGHT}-noble`;
|
|
14
14
|
|
|
15
15
|
function secretNames(dir) {
|
|
16
|
-
const path = join(dir,
|
|
16
|
+
const path = join(dir, layout(dir).config);
|
|
17
17
|
if (!existsSync(path)) return ['GO_LIVE_PASSWORD'];
|
|
18
18
|
const names = new Set();
|
|
19
19
|
const walk = (v) => {
|
|
@@ -31,7 +31,7 @@ function secretNames(dir) {
|
|
|
31
31
|
const TEMPLATES = {
|
|
32
32
|
github: {
|
|
33
33
|
file: '.github/workflows/go-live-check.yml',
|
|
34
|
-
text: (secrets) => `# Go-live check: runs on every pull request and blocks the merge when it fails.
|
|
34
|
+
text: (secrets, report) => `# Go-live check: runs on every pull request and blocks the merge when it fails.
|
|
35
35
|
# 1. Settings > Secrets and variables > Actions: add ${secrets.join(', ') || 'your test passwords'}.
|
|
36
36
|
# 2. Settings > Branches (or Rules) > protect your main branch > Require status checks: "frontend-go-live-check".
|
|
37
37
|
name: Go-live check
|
|
@@ -74,7 +74,7 @@ ${secrets.length ? ` env:\n${secrets.map((s) => ` ${s}: \${{ sec
|
|
|
74
74
|
uses: actions/upload-artifact@v4
|
|
75
75
|
with:
|
|
76
76
|
name: go-live-report
|
|
77
|
-
path:
|
|
77
|
+
path: ${report}
|
|
78
78
|
retention-days: 30
|
|
79
79
|
|
|
80
80
|
- name: Comment the result on the pull request
|
|
@@ -84,7 +84,7 @@ ${secrets.length ? ` env:\n${secrets.map((s) => ` ${s}: \${{ sec
|
|
|
84
84
|
script: |
|
|
85
85
|
const fs = require('fs');
|
|
86
86
|
const marker = '<!-- frontend-go-live-check -->';
|
|
87
|
-
const summary = fs.existsSync('
|
|
87
|
+
const summary = fs.existsSync('${report}/summary.md') ? fs.readFileSync('${report}/summary.md', 'utf8') : 'The go-live check did not finish: open the job log.';
|
|
88
88
|
const run = \`\${context.serverUrl}/\${context.repo.owner}/\${context.repo.repo}/actions/runs/\${context.runId}\`;
|
|
89
89
|
const body = \`\${marker}\\n\${summary}\\n\\nFull report, PDF and Word: the **go-live-report** artifact of [this run](\${run}).\`;
|
|
90
90
|
const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number: context.issue.number });
|
|
@@ -95,7 +95,7 @@ ${secrets.length ? ` env:\n${secrets.map((s) => ` ${s}: \${{ sec
|
|
|
95
95
|
},
|
|
96
96
|
gitlab: {
|
|
97
97
|
file: '.gitlab/go-live-check.gitlab-ci.yml',
|
|
98
|
-
text: (secrets) => `# Go-live check: runs on every merge request and blocks the merge when it fails.
|
|
98
|
+
text: (secrets, report) => `# Go-live check: runs on every merge request and blocks the merge when it fails.
|
|
99
99
|
# 1. Add to .gitlab-ci.yml: include: '.gitlab/go-live-check.gitlab-ci.yml'
|
|
100
100
|
# 2. Settings > CI/CD > Variables: add ${secrets.join(', ') || 'your test passwords'} (Masked, Protected as needed).
|
|
101
101
|
# 3. Settings > Merge requests > "Pipelines must succeed".
|
|
@@ -117,14 +117,14 @@ frontend-go-live-check:
|
|
|
117
117
|
artifacts:
|
|
118
118
|
when: always
|
|
119
119
|
expire_in: 30 days
|
|
120
|
-
paths: [
|
|
120
|
+
paths: [${report}/]
|
|
121
121
|
reports:
|
|
122
|
-
junit:
|
|
122
|
+
junit: ${report}/junit.xml
|
|
123
123
|
`,
|
|
124
124
|
},
|
|
125
125
|
azure: {
|
|
126
126
|
file: 'azure-pipelines.go-live-check.yml',
|
|
127
|
-
text: (secrets) => `# Go-live check: runs on every pull request and blocks the merge when it fails.
|
|
127
|
+
text: (secrets, report) => `# Go-live check: runs on every pull request and blocks the merge when it fails.
|
|
128
128
|
# 1. Pipelines > New pipeline > your repo > Existing YAML file > azure-pipelines.go-live-check.yml
|
|
129
129
|
# 2. Edit > Variables: add ${secrets.join(', ') || 'your test passwords'}, tick "Keep this value secret".
|
|
130
130
|
# 3. Project settings > Repositories > the repo > Policies > main > Build validation: add this pipeline, Required.
|
|
@@ -156,21 +156,21 @@ ${secrets.length ? ` env:\n${secrets.map((s) => ` ${s}: $(${s})`).join('
|
|
|
156
156
|
condition: always()
|
|
157
157
|
inputs:
|
|
158
158
|
testResultsFormat: JUnit
|
|
159
|
-
testResultsFiles:
|
|
159
|
+
testResultsFiles: ${report}/junit.xml
|
|
160
160
|
testRunTitle: Go-live check
|
|
161
161
|
displayName: Problems in the Tests tab
|
|
162
162
|
|
|
163
163
|
- task: PublishPipelineArtifact@1
|
|
164
164
|
condition: always()
|
|
165
165
|
inputs:
|
|
166
|
-
targetPath:
|
|
166
|
+
targetPath: ${report}
|
|
167
167
|
artifact: go-live-report
|
|
168
168
|
displayName: Keep the report (HTML, PDF, Word)
|
|
169
169
|
`,
|
|
170
170
|
},
|
|
171
171
|
bitbucket: {
|
|
172
172
|
file: 'bitbucket-pipelines.go-live-check.yml',
|
|
173
|
-
text: (secrets) => `# Go-live check for Bitbucket. Copy the "frontend-go-live-check" step into bitbucket-pipelines.yml.
|
|
173
|
+
text: (secrets, report) => `# Go-live check for Bitbucket. Copy the "frontend-go-live-check" step into bitbucket-pipelines.yml.
|
|
174
174
|
# 1. Repository settings > Repository variables: add ${secrets.join(', ') || 'your test passwords'} (Secured).
|
|
175
175
|
# 2. Repository settings > Branch restrictions > main > "Minimum successful builds": 1.
|
|
176
176
|
image: ${IMAGE}
|
|
@@ -186,12 +186,12 @@ pipelines:
|
|
|
186
186
|
- npm ci
|
|
187
187
|
- npx frontend-go-live-check --ci
|
|
188
188
|
artifacts:
|
|
189
|
-
-
|
|
189
|
+
- ${report}/**
|
|
190
190
|
`,
|
|
191
191
|
},
|
|
192
192
|
jenkins: {
|
|
193
193
|
file: 'Jenkinsfile.go-live-check',
|
|
194
|
-
text: (secrets) => `// Go-live check for Jenkins (multibranch pipeline: runs on every pull request).
|
|
194
|
+
text: (secrets, report) => `// Go-live check for Jenkins (multibranch pipeline: runs on every pull request).
|
|
195
195
|
// 1. Manage Jenkins > Credentials: add ${secrets.map((s) => `"${s.toLowerCase().replace(/_/g, '-')}"`).join(', ') || 'your test passwords'} as "Secret text".
|
|
196
196
|
// 2. Point the pipeline's Script Path to Jenkinsfile.go-live-check, and require it in your Git server's merge rules.
|
|
197
197
|
pipeline {
|
|
@@ -206,8 +206,8 @@ ${secrets.map((s) => ` ${s} = credentials('${s.toLowerCase().replace(/_/g, '-
|
|
|
206
206
|
}
|
|
207
207
|
post {
|
|
208
208
|
always {
|
|
209
|
-
junit allowEmptyResults: true, testResults: '
|
|
210
|
-
archiveArtifacts artifacts: '
|
|
209
|
+
junit allowEmptyResults: true, testResults: '${report}/junit.xml'
|
|
210
|
+
archiveArtifacts artifacts: '${report}/**', allowEmptyArchive: true
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -230,7 +230,7 @@ export function addPipeline(dir, provider, log = console.log) {
|
|
|
230
230
|
}
|
|
231
231
|
const secrets = secretNames(dir);
|
|
232
232
|
mkdirSync(dirname(target), { recursive: true });
|
|
233
|
-
writeFileSync(target, template.text(secrets));
|
|
233
|
+
writeFileSync(target, template.text(secrets, layout(dir).report));
|
|
234
234
|
log(`✔ added ${template.file}`);
|
|
235
235
|
log(` Read the lines at the top of the file: the secret${secrets.length === 1 ? '' : 's'} to add (${secrets.join(', ') || 'none'}) and how to make the check required for merging.`);
|
|
236
236
|
return true;
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { chromium } from 'playwright';
|
|
8
8
|
import { loadApiMap } from '../api/model.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { loadConfig } from '../core/config.mjs';
|
|
10
10
|
import { detectProject } from '../core/detect.mjs';
|
|
11
11
|
import { normaliseLogin } from '../core/login.mjs';
|
|
12
12
|
import { readSession, sessionExpiry } from '../core/session.mjs';
|
|
@@ -41,11 +41,12 @@ export async function doctor(dir, options = {}, log = console.log) {
|
|
|
41
41
|
bad(error.message);
|
|
42
42
|
return problems;
|
|
43
43
|
}
|
|
44
|
-
const { config, configFile, envFile, missingSecrets, hasApi } = loaded;
|
|
45
|
-
if (configFile) ok(`Settings: ${
|
|
46
|
-
else warn(`No ${
|
|
47
|
-
if (envFile) ok(`Test passwords file: ${
|
|
48
|
-
if (missingSecrets.length) bad(`Not set: ${missingSecrets.join(', ')} (put them in ${
|
|
44
|
+
const { config, configFile, envFile, missingSecrets, hasApi, paths } = loaded;
|
|
45
|
+
if (configFile) ok(`Settings: ${paths.config}`);
|
|
46
|
+
else warn(`No ${paths.config}: the defaults are used (run frontend-go-live-check init to create one)`);
|
|
47
|
+
if (envFile) ok(`Test passwords file: ${paths.env}`);
|
|
48
|
+
if (missingSecrets.length) bad(`Not set: ${missingSecrets.join(', ')} (put them in ${paths.env}, or the environment)`);
|
|
49
|
+
if (paths.legacy) warn('Files are at the project root (older layout). It still works; to move them into one go-live-check/ folder, see the README (Files).');
|
|
49
50
|
else if (configFile) ok('Every $NAME in the settings has a value');
|
|
50
51
|
|
|
51
52
|
if (config.url) ok(`Checks the running site ${config.url}`);
|
|
@@ -81,9 +82,9 @@ export async function doctor(dir, options = {}, log = console.log) {
|
|
|
81
82
|
} catch (error) {
|
|
82
83
|
bad(`API file: ${error.message}`);
|
|
83
84
|
}
|
|
84
|
-
} else warn(
|
|
85
|
+
} else warn(`No API file: put your Postman collection at ${paths.api}/postman_collection.json (or OpenAPI at ${paths.api}/openapi.json) to add the API tests`);
|
|
85
86
|
|
|
86
|
-
if (!existsSync(join(dir, '.gitignore'))) warn(
|
|
87
|
+
if (!existsSync(join(dir, paths.legacy ? '.gitignore' : 'go-live-check/.gitignore'))) warn(`No .gitignore: make sure ${paths.env} is never committed (run frontend-go-live-check init)`);
|
|
87
88
|
log(problems ? `\n✖ ${problems} problem(s) to fix first.\n` : '\n✔ Ready: run npm run go-live\n');
|
|
88
89
|
return problems;
|
|
89
90
|
}
|