@dargmuesli/nuxt-vio-testing 23.1.0 → 23.1.2
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.
|
@@ -7,8 +7,8 @@ export declare const testMetadata: ({ page, path, title }: {
|
|
|
7
7
|
title: string;
|
|
8
8
|
}) => Promise<void>;
|
|
9
9
|
export declare const testOgImage: (paths: {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
en: string;
|
|
11
|
+
de: string;
|
|
12
12
|
}) => void;
|
|
13
13
|
export declare const testPageLoad: (url: string, status?: number) => void;
|
|
14
14
|
export declare const testVisualRegression: (url: string, plain?: boolean) => void;
|
package/dist/e2e/utils/tests.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { SITE_URL, TIMEOUT } from "./constants.mjs";
|
|
|
2
2
|
import { vioTest } from "../fixtures/vioTest.mjs";
|
|
3
3
|
import { expect } from "@playwright/test";
|
|
4
4
|
import AxeBuilder from "@axe-core/playwright";
|
|
5
|
-
import {
|
|
5
|
+
import { withoutTrailingSlash } from "ufo";
|
|
6
6
|
//#region e2e/utils/tests.ts
|
|
7
7
|
const testA11y = (url) => vioTest.describe("a11y", () => {
|
|
8
8
|
vioTest("should not have any automatically detectable accessibility issues", async ({ defaultPage }) => {
|
|
@@ -403,17 +403,20 @@ const testMetadata = async ({ page, path, title }) => {
|
|
|
403
403
|
];
|
|
404
404
|
for (const object of meta) await expect(page.locator(object.tag + object.attributes?.map((attribute) => `[${attribute.key}="${attribute.value}"]`).join(""))).toBeAttached();
|
|
405
405
|
expect((await page.locator("script[data-hid=\"schema-org-graph\"]").innerText()).replaceAll(SITE_URL, "https://example.com")).toMatchSnapshot(`schema-org-graph-${process.env.VIO_SERVER}.json`);
|
|
406
|
+
const ogImageContent = await page.locator("meta[property=\"og:image\"]").getAttribute("content");
|
|
407
|
+
expect(ogImageContent).toBeTruthy();
|
|
408
|
+
expect(ogImageContent?.startsWith(SITE_URL)).toBeTruthy();
|
|
409
|
+
expect(ogImageContent).toMatch(/(\/_og\/[ds]\/).+\.png$/);
|
|
406
410
|
};
|
|
407
411
|
const testOgImage = (paths) => vioTest.describe("visual regression", () => {
|
|
408
412
|
vioTest("generates the open graph image", async ({ page }) => {
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await expect(page).toHaveScreenshot();
|
|
413
|
+
for (const path of [paths.en, paths.de]) {
|
|
414
|
+
await page.goto(path);
|
|
415
|
+
const ogImageUrl = await page.locator("meta[property=\"og:image\"]").getAttribute("content");
|
|
416
|
+
if (!ogImageUrl) throw new Error("The og:image url is not given.");
|
|
417
|
+
await page.goto(ogImageUrl);
|
|
418
|
+
await expect(page).toHaveScreenshot();
|
|
419
|
+
}
|
|
417
420
|
});
|
|
418
421
|
});
|
|
419
422
|
const testPageLoad = (url, status = 200) => vioTest.describe("page load", () => {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { SITE_URL, TIMEOUT } from "./e2e/utils/constants.mjs";
|
|
2
2
|
import { defineConfig, devices } from "@playwright/test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
//#region playwright.config.ts
|
|
5
|
+
const VIO_SERVER = process.env.VIO_SERVER || "dev";
|
|
6
|
+
const WARM_DEV_SERVER_SCRIPT = fileURLToPath(new URL("./scripts/warm-dev-server.mjs", import.meta.url));
|
|
4
7
|
/**
|
|
5
8
|
* Read environment variables from file.
|
|
6
9
|
* https://github.com/motdotla/dotenv
|
|
@@ -36,7 +39,7 @@ var playwright_config_default = defineConfig({
|
|
|
36
39
|
trace: "on-first-retry"
|
|
37
40
|
},
|
|
38
41
|
webServer: {
|
|
39
|
-
command: `pnpm run --dir ../src certificates && pnpm run --dir ../src start:${
|
|
42
|
+
command: VIO_SERVER === "dev" ? `pnpm run --dir ../src certificates && node ${WARM_DEV_SERVER_SCRIPT}` : `pnpm run --dir ../src certificates && pnpm run --dir ../src start:${VIO_SERVER}`,
|
|
40
43
|
env: {
|
|
41
44
|
NUXT_APP_SECRET: "vio-testing-app-secret-0123456789",
|
|
42
45
|
NUXT_PUBLIC_VIO_IS_TESTING: "true"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Runs the dev server on an internal port, warms it up, then proxies the
|
|
2
|
+
// public port to it transparently.
|
|
3
|
+
//
|
|
4
|
+
// nuxt dev answers 503 "Starting Nuxt..." while it starts up, and a request
|
|
5
|
+
// that reaches it during the switch to the real app can hang without ever
|
|
6
|
+
// being answered. Playwright's webServer availability check has no
|
|
7
|
+
// per-request timeout and does not retry while one is in flight, so a single
|
|
8
|
+
// check landing in that window would exhaust the whole timeout even though
|
|
9
|
+
// the site is up. Keeping the server on an internal port until it has
|
|
10
|
+
// answered real page loads twice in a row means the checks and the browsers
|
|
11
|
+
// only ever talk to a warm server; until the proxy opens they get connection
|
|
12
|
+
// refused, which they retry without hanging. The warmup polls use a fresh
|
|
13
|
+
// connection and a hard timeout per attempt, so a single orphaned request
|
|
14
|
+
// cannot stall them.
|
|
15
|
+
//
|
|
16
|
+
// Usage: node warm-dev-server.mjs
|
|
17
|
+
// The public port comes from PORT (as set by the e2e script); the dev server
|
|
18
|
+
// runs on PORT + 1000.
|
|
19
|
+
|
|
20
|
+
import { spawn } from 'node:child_process'
|
|
21
|
+
import https from 'node:https'
|
|
22
|
+
import net from 'node:net'
|
|
23
|
+
|
|
24
|
+
const host = process.env.HOST || 'app.localhost'
|
|
25
|
+
const publicPort = Number(process.env.PORT || 3000)
|
|
26
|
+
const internalPort = publicPort + 1000
|
|
27
|
+
const warmupBudgetMs = 120_000
|
|
28
|
+
|
|
29
|
+
const server = spawn('pnpm run --dir ../src start:dev', {
|
|
30
|
+
env: { ...process.env, PORT: String(internalPort) },
|
|
31
|
+
shell: true,
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function stop(code) {
|
|
36
|
+
server.kill('SIGTERM')
|
|
37
|
+
process.exit(code)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.on('SIGTERM', () => stop(0))
|
|
41
|
+
process.on('SIGINT', () => stop(0))
|
|
42
|
+
server.on('exit', (code) => stop(code ?? 1))
|
|
43
|
+
|
|
44
|
+
// matches playwright's own availability check: 200 to 403
|
|
45
|
+
function probe() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const request = https.get(
|
|
48
|
+
{
|
|
49
|
+
host,
|
|
50
|
+
path: '/',
|
|
51
|
+
port: internalPort,
|
|
52
|
+
// talks only to the local dev server we just spawned on an internal, un-exposed
|
|
53
|
+
// loopback port, with our own mkcert dev cert, so there is no untrusted peer to
|
|
54
|
+
// authenticate; mirrors the e2e's ignoreHTTPSErrors. We only care that it is up.
|
|
55
|
+
rejectUnauthorized: false,
|
|
56
|
+
timeout: 2500,
|
|
57
|
+
},
|
|
58
|
+
(response) => {
|
|
59
|
+
const available =
|
|
60
|
+
response.statusCode >= 200 && response.statusCode < 404
|
|
61
|
+
response.resume()
|
|
62
|
+
response.on('end', () => resolve(available))
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
request.on('error', () => resolve(false))
|
|
66
|
+
request.on('timeout', () => {
|
|
67
|
+
request.destroy()
|
|
68
|
+
resolve(false)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function waitForWarmup() {
|
|
74
|
+
const start = Date.now()
|
|
75
|
+
let consecutive = 0
|
|
76
|
+
while (Date.now() - start < warmupBudgetMs) {
|
|
77
|
+
if (await probe()) consecutive += 1
|
|
78
|
+
else consecutive = 0
|
|
79
|
+
if (consecutive >= 2) return true
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function openProxy() {
|
|
86
|
+
const proxy = net.createServer((socket) => {
|
|
87
|
+
const upstream = net.connect({ host: '127.0.0.1', port: internalPort })
|
|
88
|
+
socket.pipe(upstream)
|
|
89
|
+
upstream.pipe(socket)
|
|
90
|
+
socket.on('error', () => upstream.destroy())
|
|
91
|
+
upstream.on('error', () => socket.destroy())
|
|
92
|
+
})
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
proxy.once('error', reject)
|
|
95
|
+
proxy.listen(publicPort, resolve)
|
|
96
|
+
})
|
|
97
|
+
return proxy
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!(await waitForWarmup())) {
|
|
101
|
+
process.stderr.write('dev server did not warm up in time\n')
|
|
102
|
+
stop(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let proxy
|
|
106
|
+
try {
|
|
107
|
+
proxy = await openProxy()
|
|
108
|
+
} catch (error) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`could not open the proxy on port ${publicPort}: ${error.message}\n`,
|
|
111
|
+
)
|
|
112
|
+
stop(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
proxy.on('close', () => stop(0))
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"@types/lodash-es": "4.17.12",
|
|
7
7
|
"@types/node": "24.19.2",
|
|
8
8
|
"cross-env": "10.1.0",
|
|
9
|
-
"eslint": "10.
|
|
9
|
+
"eslint": "10.13.0",
|
|
10
10
|
"eslint-config-prettier": "10.1.8",
|
|
11
11
|
"eslint-plugin-prettier": "5.5.7",
|
|
12
12
|
"lodash-es": "4.18.1",
|
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
"url": "git+https://github.com/dargmuesli/vio.git"
|
|
57
57
|
},
|
|
58
58
|
"type": "module",
|
|
59
|
-
"version": "23.1.
|
|
59
|
+
"version": "23.1.2",
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|
|
62
|
+
"postbuild": "node -e \"import('node:fs').then(fs=>{fs.mkdirSync('dist/scripts',{recursive:true});fs.copyFileSync('scripts/warm-dev-server.mjs','dist/scripts/warm-dev-server.mjs')})\"",
|
|
62
63
|
"build:watch": "tsdown --watch",
|
|
63
64
|
"lint": "pnpm run lint:js && pnpm run lint:ts",
|
|
64
65
|
"lint:fix": "pnpm run lint:js --fix",
|