fimo 0.6.0 → 0.7.0-experimental.10
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/assets/skills/fimo/SKILL.md +2 -2
- package/assets/skills/fimo/references/locales.md +0 -8
- package/assets/skills/fimo/references/setup-react-router.md +2 -2
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/references/content.md +0 -3
- package/assets/skills/fimo-cli/references/locales.md +0 -6
- package/assets/skills/fimo-migration/SKILL.md +22 -2
- package/assets/skills/fimo-migration/references/content.md +8 -0
- package/assets/skills/fimo-migration/references/manifest.md +35 -0
- package/assets/skills/fimo-migration/references/qa.md +24 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +617 -0
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.d.ts.map +1 -1
- package/dist/build/vite/plugins/translations.js +3 -121
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +29490 -21165
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
- package/dist/runtime/app/FimoProviders.js +2 -23
- package/dist/runtime/astro/index.d.ts +2 -0
- package/dist/runtime/astro/index.d.ts.map +1 -0
- package/dist/runtime/astro/index.js +3 -0
- package/dist/runtime/astro/server.d.ts +3 -0
- package/dist/runtime/astro/server.d.ts.map +1 -0
- package/dist/runtime/astro/server.js +6 -0
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
- package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextProviders.js +20 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.d.ts.map +1 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +2 -0
- package/dist/runtime/next/server.d.ts.map +1 -0
- package/dist/runtime/next/server.js +4 -0
- package/dist/runtime/nuxt/composables.d.ts +9 -0
- package/dist/runtime/nuxt/composables.d.ts.map +1 -0
- package/dist/runtime/nuxt/composables.js +57 -0
- package/dist/runtime/nuxt/index.d.ts +3 -0
- package/dist/runtime/nuxt/index.d.ts.map +1 -0
- package/dist/runtime/nuxt/index.js +6 -0
- package/dist/runtime/nuxt/server.d.ts +3 -0
- package/dist/runtime/nuxt/server.d.ts.map +1 -0
- package/dist/runtime/nuxt/server.js +6 -0
- package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +5 -15
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-hook.js +41 -0
- package/dist/runtime/primitives/labels-store.d.ts +12 -0
- package/dist/runtime/primitives/labels-store.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
- package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.test.js +66 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.d.ts.map +1 -1
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
- package/dist/runtime/react/FimoSpaProviders.d.ts.map +1 -0
- package/dist/runtime/react/FimoSpaProviders.js +63 -0
- package/dist/runtime/react/index.d.ts +33 -0
- package/dist/runtime/react/index.d.ts.map +1 -0
- package/dist/runtime/react/index.js +39 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts.map +1 -0
- package/dist/runtime/remix/FimoRemixProviders.js +12 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts.map +1 -0
- package/dist/runtime/remix/FimoRemixScripts.js +19 -0
- package/dist/runtime/remix/index.d.ts +12 -0
- package/dist/runtime/remix/index.d.ts.map +1 -0
- package/dist/runtime/remix/index.js +11 -0
- package/dist/runtime/remix/server.d.ts +2 -0
- package/dist/runtime/remix/server.d.ts.map +1 -0
- package/dist/runtime/remix/server.js +2 -0
- package/dist/runtime/seo/buildSeoForPath.d.ts +3 -1
- package/dist/runtime/seo/buildSeoForPath.d.ts.map +1 -1
- package/dist/runtime/seo/buildSeoForPath.js +1 -4
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.d.ts.map +1 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
- package/dist/runtime/shared/i18n-messages.d.ts +29 -0
- package/dist/runtime/shared/i18n-messages.d.ts.map +1 -0
- package/dist/runtime/shared/i18n-messages.js +79 -0
- package/dist/runtime/shared/i18n-messages.test.d.ts +2 -0
- package/dist/runtime/shared/i18n-messages.test.d.ts.map +1 -0
- package/dist/runtime/shared/i18n-messages.test.js +39 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts.map +1 -0
- package/dist/runtime/shared/labels-fetch.pure.js +71 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
- package/dist/runtime/shared/labels-fetch.server.js +97 -0
- package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
- package/dist/runtime/shared/labels-t.pure.d.ts.map +1 -0
- package/dist/runtime/shared/labels-t.pure.js +10 -0
- package/dist/runtime/strapi/client.d.ts +26 -0
- package/dist/runtime/strapi/client.d.ts.map +1 -0
- package/dist/runtime/strapi/client.js +88 -0
- package/dist/runtime/strapi/client.test.d.ts +2 -0
- package/dist/runtime/strapi/client.test.d.ts.map +1 -0
- package/dist/runtime/strapi/client.test.js +64 -0
- package/dist/runtime/strapi/index.d.ts +5 -0
- package/dist/runtime/strapi/index.d.ts.map +1 -0
- package/dist/runtime/strapi/index.js +6 -0
- package/dist/runtime/strapi/reshape.d.ts +28 -0
- package/dist/runtime/strapi/reshape.d.ts.map +1 -0
- package/dist/runtime/strapi/reshape.js +79 -0
- package/dist/runtime/strapi/reshape.test.d.ts +2 -0
- package/dist/runtime/strapi/reshape.test.d.ts.map +1 -0
- package/dist/runtime/strapi/reshape.test.js +33 -0
- package/dist/runtime/svelte/index.d.ts +3 -0
- package/dist/runtime/svelte/index.d.ts.map +1 -0
- package/dist/runtime/svelte/index.js +7 -0
- package/dist/runtime/svelte/server.d.ts +3 -0
- package/dist/runtime/svelte/server.d.ts.map +1 -0
- package/dist/runtime/svelte/server.js +6 -0
- package/dist/runtime/vue/index.d.ts +3 -0
- package/dist/runtime/vue/index.d.ts.map +1 -0
- package/dist/runtime/vue/index.js +5 -0
- package/dist/runtime/vue/server.d.ts +3 -0
- package/dist/runtime/vue/server.d.ts.map +1 -0
- package/dist/runtime/vue/server.js +6 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoFileLink.js +19 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoText.js +20 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts.map +1 -0
- package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts.map +1 -0
- package/dist/runtime/wrapped/WrappedProviders.js +101 -0
- package/dist/runtime/wrapped/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.d.ts.map +1 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/dom.test.d.ts +2 -0
- package/dist/runtime/wrapped/dom.test.d.ts.map +1 -0
- package/dist/runtime/wrapped/dom.test.js +16 -0
- package/dist/runtime/wrapped/server.d.ts +77 -0
- package/dist/runtime/wrapped/server.d.ts.map +1 -0
- package/dist/runtime/wrapped/server.js +237 -0
- package/dist/runtime/wrapped/server.test.d.ts +2 -0
- package/dist/runtime/wrapped/server.test.d.ts.map +1 -0
- package/dist/runtime/wrapped/server.test.js +91 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +132 -0
- package/dist/scripts/apply-wrap-templates.test.d.ts +1 -0
- package/dist/scripts/apply-wrap-templates.test.js +199 -0
- package/dist/scripts/apply-wrap.d.ts +46 -0
- package/dist/scripts/apply-wrap.js +278 -0
- package/dist/scripts/apply-wrap.test.d.ts +1 -0
- package/dist/scripts/apply-wrap.test.js +226 -0
- package/dist/scripts/cms/discover.d.ts +2 -0
- package/dist/scripts/cms/discover.js +17 -0
- package/dist/scripts/cms/example-content.d.ts +32 -0
- package/dist/scripts/cms/example-content.js +108 -0
- package/dist/scripts/cms/example-content.test.d.ts +1 -0
- package/dist/scripts/cms/example-content.test.js +66 -0
- package/dist/scripts/cms/strapi/detect.d.ts +2 -0
- package/dist/scripts/cms/strapi/detect.js +186 -0
- package/dist/scripts/cms/strapi/detect.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/detect.test.js +56 -0
- package/dist/scripts/cms/strapi/index.d.ts +3 -0
- package/dist/scripts/cms/strapi/index.js +25 -0
- package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
- package/dist/scripts/cms/strapi/list-source.js +90 -0
- package/dist/scripts/cms/strapi/list-source.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/list-source.test.js +33 -0
- package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
- package/dist/scripts/cms/strapi/schema-map.js +149 -0
- package/dist/scripts/cms/strapi/schema-map.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/schema-map.test.js +91 -0
- package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
- package/dist/scripts/cms/strapi/touchpoints.js +145 -0
- package/dist/scripts/cms/strapi/touchpoints.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/touchpoints.test.js +62 -0
- package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
- package/dist/scripts/cms/strapi/wire-client.js +107 -0
- package/dist/scripts/cms/strapi/wire-client.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/wire-client.test.js +51 -0
- package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
- package/dist/scripts/cms/strapi/wiring.js +36 -0
- package/dist/scripts/cms/strapi/wiring.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/wiring.test.js +27 -0
- package/dist/scripts/cms/types.d.ts +117 -0
- package/dist/scripts/cms/types.js +11 -0
- package/dist/scripts/detect-framework.d.ts +39 -0
- package/dist/scripts/detect-framework.js +133 -0
- package/dist/scripts/detect-framework.test.d.ts +1 -0
- package/dist/scripts/detect-framework.test.js +122 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +135 -0
- package/dist/scripts/draft-rows.test.d.ts +1 -0
- package/dist/scripts/draft-rows.test.js +113 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +188 -0
- package/dist/scripts/draft-schemas.test.d.ts +1 -0
- package/dist/scripts/draft-schemas.test.js +152 -0
- package/dist/scripts/extract-translations.js +16 -11
- package/dist/scripts/extract-translations.test.d.ts +1 -0
- package/dist/scripts/extract-translations.test.js +66 -0
- package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
- package/dist/scripts/frameworks/astro/routes.js +55 -0
- package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
- package/dist/scripts/frameworks/astro/template-style.js +19 -0
- package/dist/scripts/frameworks/discover.d.ts +70 -0
- package/dist/scripts/frameworks/discover.js +178 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
- package/dist/scripts/frameworks/next/rewire.js +258 -0
- package/dist/scripts/frameworks/next/rewire.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/rewire.test.js +134 -0
- package/dist/scripts/frameworks/next/routes.d.ts +17 -0
- package/dist/scripts/frameworks/next/routes.js +134 -0
- package/dist/scripts/frameworks/next/routes.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/routes.test.js +64 -0
- package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
- package/dist/scripts/frameworks/next/wiring.js +186 -0
- package/dist/scripts/frameworks/next/wiring.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/wiring.test.js +174 -0
- package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
- package/dist/scripts/frameworks/nuxt/routes.js +54 -0
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
- package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
- package/dist/scripts/frameworks/remix/routes.js +50 -0
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/support.test.d.ts +1 -0
- package/dist/scripts/frameworks/support.test.js +87 -0
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
- package/dist/scripts/frameworks/sveltekit/routes.js +50 -0
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
- package/dist/scripts/frameworks/vite/routes.d.ts +15 -0
- package/dist/scripts/frameworks/vite/routes.js +93 -0
- package/dist/scripts/frameworks/vite/routes.test.d.ts +1 -0
- package/dist/scripts/frameworks/vite/routes.test.js +73 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
- package/dist/scripts/frameworks/vite/wiring.js +99 -0
- package/dist/scripts/frameworks/vite/wiring.test.d.ts +1 -0
- package/dist/scripts/frameworks/vite/wiring.test.js +88 -0
- package/dist/scripts/i18n-namespace.d.ts +25 -0
- package/dist/scripts/i18n-namespace.js +53 -0
- package/dist/scripts/lib/validation-report.d.ts +1 -0
- package/dist/scripts/lib/validation-report.js +3 -0
- package/dist/scripts/render-parity/capture.d.ts +56 -0
- package/dist/scripts/render-parity/capture.js +366 -0
- package/dist/scripts/render-parity/capture.test.d.ts +1 -0
- package/dist/scripts/render-parity/capture.test.js +306 -0
- package/dist/scripts/render-parity/compare.d.ts +59 -0
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/compare.test.d.ts +1 -0
- package/dist/scripts/render-parity/compare.test.js +98 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
- package/dist/scripts/render-parity/normalize-html.js +223 -0
- package/dist/scripts/render-parity/normalize-html.test.d.ts +1 -0
- package/dist/scripts/render-parity/normalize-html.test.js +58 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- package/dist/scripts/resolve-routes.js +1 -1
- package/dist/scripts/scan-wrappable.d.ts +196 -0
- package/dist/scripts/scan-wrappable.js +1227 -0
- package/dist/scripts/scan-wrappable.test.d.ts +1 -0
- package/dist/scripts/scan-wrappable.test.js +521 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/package.json +92 -5
- package/release.json +2 -2
- package/templates/react-router/package.json +7 -10
- package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
- package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
- package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { captureParityBaseline, CaptureError, ensureWorkspaceDepsBuilt, findFreePort, resolveRoutePath, } from './capture.js';
|
|
6
|
+
let dir;
|
|
7
|
+
/**
|
|
8
|
+
* Hermetic fixture: a project whose `dev` script is a tiny node server
|
|
9
|
+
* honoring PORT — exercises the whole engine without Next.
|
|
10
|
+
*/
|
|
11
|
+
const SERVER_JS = `
|
|
12
|
+
const http = require('http');
|
|
13
|
+
http
|
|
14
|
+
.createServer((req, res) => {
|
|
15
|
+
if (req.url === '/about') {
|
|
16
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
17
|
+
res.end('<html><body><h1>About us</h1><p>We are <strong>honest</strong>.</p></body></html>');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (req.url === '/broken') {
|
|
21
|
+
res.writeHead(500, { 'content-type': 'text/html' });
|
|
22
|
+
res.end('<html><body><p>error page placeholder</p></body></html>');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
26
|
+
res.end('<html><body><h1>Home</h1><img src="/hero.png"/><a href="/whitepaper.pdf">PDF</a></body></html>');
|
|
27
|
+
})
|
|
28
|
+
.listen(process.env.PORT, '127.0.0.1');
|
|
29
|
+
`;
|
|
30
|
+
/**
|
|
31
|
+
* Vite-family behavior (Astro, Nuxt, SvelteKit): ignores PORT, picks its own
|
|
32
|
+
* port, binds the IPv6 loopback only, and announces itself with a
|
|
33
|
+
* `localhost:<port>` banner. The engine must sniff the banner and probe via
|
|
34
|
+
* the hostname (a numeric 127.0.0.1 probe never reaches a ::1-only server).
|
|
35
|
+
*/
|
|
36
|
+
const IPV6_SNIFF_SERVER_JS = `
|
|
37
|
+
const http = require('http');
|
|
38
|
+
const server = http.createServer((req, res) => {
|
|
39
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
40
|
+
res.end('<html><body><h1>Home</h1></body></html>');
|
|
41
|
+
});
|
|
42
|
+
const announce = () => console.log(' | Local http://localhost:' + server.address().port + '/');
|
|
43
|
+
server.listen(0, '::1', announce).on('error', () => server.listen(0, '127.0.0.1', announce));
|
|
44
|
+
`;
|
|
45
|
+
/**
|
|
46
|
+
* Nuxt-dev behavior: the HTTP socket answers immediately but serves 503
|
|
47
|
+
* until the app finishes building. Readiness must wait for a sub-500.
|
|
48
|
+
*/
|
|
49
|
+
const BOOT_503_SERVER_JS = `
|
|
50
|
+
const http = require('http');
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
http
|
|
53
|
+
.createServer((req, res) => {
|
|
54
|
+
if (Date.now() - startedAt < 2000) {
|
|
55
|
+
res.writeHead(503, { 'content-type': 'text/html' });
|
|
56
|
+
res.end('<html><body>Server is starting</body></html>');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
60
|
+
res.end('<html><body><h1>Home</h1></body></html>');
|
|
61
|
+
})
|
|
62
|
+
.listen(process.env.PORT, '127.0.0.1');
|
|
63
|
+
`;
|
|
64
|
+
const project = (opts = {}) => {
|
|
65
|
+
dir = mkdtempSync(join(tmpdir(), 'parity-capture-'));
|
|
66
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 't', scripts: { dev: opts.devScript ?? 'node server.js' } }));
|
|
67
|
+
writeFileSync(join(dir, 'server.js'), SERVER_JS);
|
|
68
|
+
writeFileSync(join(dir, 'server-ipv6.js'), IPV6_SNIFF_SERVER_JS);
|
|
69
|
+
writeFileSync(join(dir, 'server-503-then-200.js'), BOOT_503_SERVER_JS);
|
|
70
|
+
if (opts.withNodeModules !== false) {
|
|
71
|
+
mkdirSync(join(dir, 'node_modules'), { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
return dir;
|
|
74
|
+
};
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
if (dir) {
|
|
77
|
+
rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
describe('findFreePort', () => {
|
|
81
|
+
it('returns a usable port', async () => {
|
|
82
|
+
const port = await findFreePort();
|
|
83
|
+
expect(port).toBeGreaterThan(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('ensureWorkspaceDepsBuilt (monorepo pre-build)', () => {
|
|
87
|
+
const monorepo = (opts) => {
|
|
88
|
+
dir = mkdtempSync(join(tmpdir(), 'ws-build-'));
|
|
89
|
+
writeFileSync(join(dir, 'pnpm-workspace.yaml'), "packages:\n - 'apps/*'\n - 'packages/*'\n");
|
|
90
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'root', private: true, scripts: opts.rootBuild ? { build: opts.rootBuild } : {} }));
|
|
91
|
+
// The app depends on an internal workspace package.
|
|
92
|
+
mkdirSync(join(dir, 'apps/ui'), { recursive: true });
|
|
93
|
+
writeFileSync(join(dir, 'apps/ui/package.json'), JSON.stringify({ name: '@repo/ui', dependencies: { '@repo/shared': 'workspace:*' } }));
|
|
94
|
+
// The internal package, symlink-style resolved under the app's node_modules.
|
|
95
|
+
const depDir = join(dir, 'apps/ui/node_modules/@repo/shared');
|
|
96
|
+
mkdirSync(depDir, { recursive: true });
|
|
97
|
+
writeFileSync(join(depDir, 'package.json'), JSON.stringify({ name: '@repo/shared', main: 'dist/index.js' }));
|
|
98
|
+
if (opts.depBuilt) {
|
|
99
|
+
mkdirSync(join(depDir, 'dist'), { recursive: true });
|
|
100
|
+
writeFileSync(join(depDir, 'dist/index.js'), 'module.exports = {};');
|
|
101
|
+
}
|
|
102
|
+
return { root: dir, app: join(dir, 'apps/ui'), depDir };
|
|
103
|
+
};
|
|
104
|
+
it('builds the app`s unbuilt workspace deps via the root build script', () => {
|
|
105
|
+
// The root build script materializes the missing dist so a later boot works.
|
|
106
|
+
const build = "node -e \"require('fs').mkdirSync('apps/ui/node_modules/@repo/shared/dist',{recursive:true});require('fs').writeFileSync('apps/ui/node_modules/@repo/shared/dist/index.js','')\"";
|
|
107
|
+
const { app, depDir } = monorepo({ rootBuild: build, depBuilt: false });
|
|
108
|
+
ensureWorkspaceDepsBuilt(app, 'npm');
|
|
109
|
+
expect(existsSync(join(depDir, 'dist/index.js'))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('is a no-op when the workspace deps are already built', () => {
|
|
112
|
+
const { app } = monorepo({ rootBuild: 'exit 1', depBuilt: true });
|
|
113
|
+
// depBuilt → nothing to build → the failing build script is never run.
|
|
114
|
+
expect(() => ensureWorkspaceDepsBuilt(app, 'npm')).not.toThrow();
|
|
115
|
+
});
|
|
116
|
+
it('does nothing for a standalone (non-monorepo) app', () => {
|
|
117
|
+
dir = mkdtempSync(join(tmpdir(), 'standalone-'));
|
|
118
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'solo', dependencies: { next: '^15.0.0' } }));
|
|
119
|
+
expect(() => ensureWorkspaceDepsBuilt(dir, 'npm')).not.toThrow();
|
|
120
|
+
});
|
|
121
|
+
it('throws an actionable error when the build fails', () => {
|
|
122
|
+
const { app } = monorepo({ rootBuild: 'exit 1', depBuilt: false });
|
|
123
|
+
expect(() => ensureWorkspaceDepsBuilt(app, 'npm')).toThrow(/need building|failed/i);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('resolveRoutePath (dynamic param substitution)', () => {
|
|
127
|
+
it('substitutes known params and reports unresolved ones', () => {
|
|
128
|
+
expect(resolveRoutePath('/:locale/blog', { locale: 'en' })).toBe('/en/blog');
|
|
129
|
+
expect(resolveRoutePath('/:locale', { locale: 'fr' })).toBe('/fr');
|
|
130
|
+
// A content param with no value stays unresolved → the route is skipped.
|
|
131
|
+
expect(resolveRoutePath('/:locale/blog/:slug', { locale: 'en' })).toBeNull();
|
|
132
|
+
expect(resolveRoutePath('/blog/:slug', { locale: 'en' })).toBeNull();
|
|
133
|
+
expect(resolveRoutePath('/static', {})).toBe('/static');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('captureParityBaseline', () => {
|
|
137
|
+
it('boots the dev server, captures routes, and tears down', async () => {
|
|
138
|
+
const cwd = project();
|
|
139
|
+
const baseline = await captureParityBaseline({
|
|
140
|
+
cwd,
|
|
141
|
+
packageManager: 'npm',
|
|
142
|
+
framework: 'next',
|
|
143
|
+
routes: [{ path: '/' }, { path: '/about' }, { path: '/blog/:slug' }],
|
|
144
|
+
bootTimeoutMs: 30_000,
|
|
145
|
+
routeTimeoutMs: 10_000,
|
|
146
|
+
});
|
|
147
|
+
expect(baseline.version).toBe(1);
|
|
148
|
+
const home = baseline.routes['/'];
|
|
149
|
+
expect('signature' in home && home.signature.blocks[0]).toEqual({ tag: 'h1', text: 'Home', marks: [] });
|
|
150
|
+
expect('signature' in home && home.signature.images).toEqual(['/hero.png']);
|
|
151
|
+
const about = baseline.routes['/about'];
|
|
152
|
+
expect('signature' in about && about.signature.blocks[1].marks[0]).toEqual({ kind: 'strong', text: 'honest' });
|
|
153
|
+
expect(baseline.routes['/blog/:slug']).toEqual({ skipped: 'dynamic-params' });
|
|
154
|
+
}, 60_000);
|
|
155
|
+
it('captures [locale] routes by substituting the param (i18n sites are no longer all-skipped)', async () => {
|
|
156
|
+
const cwd = project();
|
|
157
|
+
const baseline = await captureParityBaseline({
|
|
158
|
+
cwd,
|
|
159
|
+
packageManager: 'npm',
|
|
160
|
+
framework: 'next',
|
|
161
|
+
routes: [{ path: '/:locale' }, { path: '/:locale/blog/:slug' }],
|
|
162
|
+
paramValues: { locale: 'en' },
|
|
163
|
+
bootTimeoutMs: 30_000,
|
|
164
|
+
routeTimeoutMs: 10_000,
|
|
165
|
+
});
|
|
166
|
+
// Keyed by the ORIGINAL path, captured from the resolved /en URL.
|
|
167
|
+
const localeRoot = baseline.routes['/:locale'];
|
|
168
|
+
expect('signature' in localeRoot && localeRoot.signature.blocks[0].text).toBe('Home');
|
|
169
|
+
// A content param (:slug) still can't be resolved → skipped.
|
|
170
|
+
expect(baseline.routes['/:locale/blog/:slug']).toEqual({ skipped: 'dynamic-params' });
|
|
171
|
+
}, 60_000);
|
|
172
|
+
it('records 5xx status as part of the signature', async () => {
|
|
173
|
+
const cwd = project();
|
|
174
|
+
const baseline = await captureParityBaseline({
|
|
175
|
+
cwd,
|
|
176
|
+
packageManager: 'npm',
|
|
177
|
+
framework: 'next',
|
|
178
|
+
routes: [{ path: '/broken' }],
|
|
179
|
+
bootTimeoutMs: 30_000,
|
|
180
|
+
routeTimeoutMs: 10_000,
|
|
181
|
+
});
|
|
182
|
+
const broken = baseline.routes['/broken'];
|
|
183
|
+
expect('signature' in broken && broken.signature.status).toBe(500);
|
|
184
|
+
}, 60_000);
|
|
185
|
+
it('waits through boot-time 503s (nuxi answers before the app is built)', async () => {
|
|
186
|
+
const cwd = project({ devScript: 'node server-503-then-200.js' });
|
|
187
|
+
const baseline = await captureParityBaseline({
|
|
188
|
+
cwd,
|
|
189
|
+
packageManager: 'npm',
|
|
190
|
+
framework: 'nuxt',
|
|
191
|
+
routes: [{ path: '/' }],
|
|
192
|
+
bootTimeoutMs: 30_000,
|
|
193
|
+
routeTimeoutMs: 10_000,
|
|
194
|
+
});
|
|
195
|
+
const home = baseline.routes['/'];
|
|
196
|
+
expect('signature' in home && home.signature.status).toBe(200);
|
|
197
|
+
}, 60_000);
|
|
198
|
+
it('finds a PORT-ignoring, ::1-bound server through its localhost banner', async () => {
|
|
199
|
+
const cwd = project({ devScript: 'node server-ipv6.js' });
|
|
200
|
+
const baseline = await captureParityBaseline({
|
|
201
|
+
cwd,
|
|
202
|
+
packageManager: 'npm',
|
|
203
|
+
framework: 'astro',
|
|
204
|
+
routes: [{ path: '/' }],
|
|
205
|
+
bootTimeoutMs: 30_000,
|
|
206
|
+
routeTimeoutMs: 10_000,
|
|
207
|
+
});
|
|
208
|
+
const home = baseline.routes['/'];
|
|
209
|
+
expect('signature' in home && home.signature.status).toBe(200);
|
|
210
|
+
}, 60_000);
|
|
211
|
+
it('fails fast without node_modules', async () => {
|
|
212
|
+
const cwd = project({ withNodeModules: false });
|
|
213
|
+
await expect(captureParityBaseline({
|
|
214
|
+
cwd,
|
|
215
|
+
packageManager: 'npm',
|
|
216
|
+
framework: null,
|
|
217
|
+
routes: [{ path: '/' }],
|
|
218
|
+
bootTimeoutMs: 5_000,
|
|
219
|
+
})).rejects.toThrow(CaptureError);
|
|
220
|
+
});
|
|
221
|
+
it('surfaces stderr when the dev server dies at boot', async () => {
|
|
222
|
+
const cwd = project({ devScript: 'node -e "console.error(\'FATAL: nope\'); process.exit(1)"' });
|
|
223
|
+
await expect(captureParityBaseline({
|
|
224
|
+
cwd,
|
|
225
|
+
packageManager: 'npm',
|
|
226
|
+
framework: null,
|
|
227
|
+
routes: [{ path: '/' }],
|
|
228
|
+
bootTimeoutMs: 15_000,
|
|
229
|
+
})).rejects.toMatchObject({ stderrTail: expect.stringContaining('FATAL: nope') });
|
|
230
|
+
}, 30_000);
|
|
231
|
+
});
|
|
232
|
+
it('retries error routes once the server is warm instead of poisoning the baseline', async () => {
|
|
233
|
+
const { mkdtempSync, writeFileSync, rmSync, mkdirSync } = await import('node:fs');
|
|
234
|
+
const { tmpdir } = await import('node:os');
|
|
235
|
+
const { join } = await import('node:path');
|
|
236
|
+
const dir = mkdtempSync(join(tmpdir(), 'warm-retry-'));
|
|
237
|
+
try {
|
|
238
|
+
mkdirSync(join(dir, 'node_modules'), { recursive: true });
|
|
239
|
+
// First hit on /flaky answers 500 (cold compile), later hits answer 200.
|
|
240
|
+
writeFileSync(join(dir, 'server.js'), `const http = require('http');
|
|
241
|
+
let flakyHits = 0;
|
|
242
|
+
http.createServer((req, res) => {
|
|
243
|
+
if (req.url.startsWith('/flaky')) {
|
|
244
|
+
flakyHits += 1;
|
|
245
|
+
if (flakyHits <= 2) { res.writeHead(500); res.end(''); return; }
|
|
246
|
+
res.writeHead(200, {'content-type': 'text/html'});
|
|
247
|
+
res.end('<html><body><h1>Recovered</h1></body></html>');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
res.writeHead(200, {'content-type': 'text/html'});
|
|
251
|
+
res.end('<html><body><h1>Home</h1></body></html>');
|
|
252
|
+
}).listen(process.env.PORT || 0, '127.0.0.1');`);
|
|
253
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ scripts: { dev: 'node server.js' } }));
|
|
254
|
+
const baseline = await captureParityBaseline({
|
|
255
|
+
cwd: dir,
|
|
256
|
+
packageManager: 'npm',
|
|
257
|
+
routes: [{ path: '/' }, { path: '/flaky' }],
|
|
258
|
+
framework: 'next',
|
|
259
|
+
bootTimeoutMs: 20_000,
|
|
260
|
+
routeTimeoutMs: 5_000,
|
|
261
|
+
});
|
|
262
|
+
const flaky = baseline.routes['/flaky'];
|
|
263
|
+
expect(flaky && 'signature' in flaky ? flaky.signature.status : null).toBe(200);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
rmSync(dir, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
}, 30_000);
|
|
269
|
+
it('reuses a running server via serverOrigin without booting or stopping it', async () => {
|
|
270
|
+
const http = await import('node:http');
|
|
271
|
+
const server = http.createServer((_req, res) => {
|
|
272
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
273
|
+
res.end('<html><body><h1>Warm</h1></body></html>');
|
|
274
|
+
});
|
|
275
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
276
|
+
const address = server.address();
|
|
277
|
+
const origin = `http://127.0.0.1:${typeof address === 'object' && address ? address.port : 0}`;
|
|
278
|
+
try {
|
|
279
|
+
// cwd with no dev script and no node_modules: booting would fail loudly.
|
|
280
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
281
|
+
const { tmpdir } = await import('node:os');
|
|
282
|
+
const { join } = await import('node:path');
|
|
283
|
+
const dir = mkdtempSync(join(tmpdir(), 'warm-origin-'));
|
|
284
|
+
try {
|
|
285
|
+
const baseline = await captureParityBaseline({
|
|
286
|
+
cwd: dir,
|
|
287
|
+
packageManager: 'npm',
|
|
288
|
+
routes: [{ path: '/' }],
|
|
289
|
+
framework: 'next',
|
|
290
|
+
serverOrigin: origin,
|
|
291
|
+
routeTimeoutMs: 5_000,
|
|
292
|
+
});
|
|
293
|
+
const route = baseline.routes['/'];
|
|
294
|
+
expect(route && 'signature' in route ? route.signature.status : null).toBe(200);
|
|
295
|
+
// the server is still ours and still alive
|
|
296
|
+
const probe = await fetch(origin);
|
|
297
|
+
expect(probe.status).toBe(200);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
server.close();
|
|
305
|
+
}
|
|
306
|
+
}, 20_000);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { InlineMark, ParityBaseline } from './types.js';
|
|
2
|
+
export interface BlockFinding {
|
|
3
|
+
kind: 'text-changed' | 'block-missing' | 'block-added' | 'marks-changed';
|
|
4
|
+
index: number;
|
|
5
|
+
tag?: string;
|
|
6
|
+
before?: string;
|
|
7
|
+
after?: string;
|
|
8
|
+
marksBefore?: InlineMark[];
|
|
9
|
+
marksAfter?: InlineMark[];
|
|
10
|
+
}
|
|
11
|
+
export interface MediaFinding {
|
|
12
|
+
kind: 'changed' | 'missing' | 'added' | 'rehosted';
|
|
13
|
+
index: number;
|
|
14
|
+
before?: string;
|
|
15
|
+
after?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LinkFinding {
|
|
18
|
+
kind: 'changed' | 'missing' | 'added';
|
|
19
|
+
index: number;
|
|
20
|
+
before?: string;
|
|
21
|
+
after?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RouteDiff {
|
|
24
|
+
route: string;
|
|
25
|
+
clean: boolean;
|
|
26
|
+
unverified?: boolean;
|
|
27
|
+
statusChange?: {
|
|
28
|
+
before: number;
|
|
29
|
+
after: number;
|
|
30
|
+
};
|
|
31
|
+
blocks: BlockFinding[];
|
|
32
|
+
images: MediaFinding[];
|
|
33
|
+
videos: MediaFinding[];
|
|
34
|
+
links: LinkFinding[];
|
|
35
|
+
notes: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface ParityComparison {
|
|
38
|
+
diffs: RouteDiff[];
|
|
39
|
+
clean: number;
|
|
40
|
+
drifted: number;
|
|
41
|
+
unverified: number;
|
|
42
|
+
}
|
|
43
|
+
export interface CompareOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Full-match regexes for text that is dynamic BY THE SITE'S OWN DESIGN
|
|
46
|
+
* (live clocks, visitor counters). A block whose text fully matches the
|
|
47
|
+
* same pattern in baseline and current compares as equal. Recorded in
|
|
48
|
+
* `.fimo/migration/manifest.json` → `wrap.parityIgnores` with a reason.
|
|
49
|
+
*/
|
|
50
|
+
ignoreTextPatterns?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Routes whose content drifts by DECISION — they render a keep-live
|
|
53
|
+
* external data source (`wrap.dataSources` → live). Counted as
|
|
54
|
+
* unverified, never silently clean.
|
|
55
|
+
*/
|
|
56
|
+
ignoreRoutes?: string[];
|
|
57
|
+
}
|
|
58
|
+
export declare function compareBaselines(baseline: ParityBaseline, current: ParityBaseline, options?: CompareOptions): ParityComparison;
|
|
59
|
+
export declare function formatRouteDiff(diff: RouteDiff): string[];
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { compareBaselines, formatRouteDiff } from './compare.js';
|
|
3
|
+
const signature = (over = {}) => ({
|
|
4
|
+
status: 200,
|
|
5
|
+
blocks: [
|
|
6
|
+
{ tag: 'h1', text: 'Ship faster', marks: [] },
|
|
7
|
+
{ tag: 'p', text: 'Read honest numbers today', marks: [{ kind: 'strong', text: 'honest numbers' }] },
|
|
8
|
+
{ tag: 'p', text: 'Get early access', marks: [] },
|
|
9
|
+
],
|
|
10
|
+
images: ['/images/hero.png'],
|
|
11
|
+
videos: [],
|
|
12
|
+
links: [{ href: '/whitepaper.pdf', text: 'PDF' }],
|
|
13
|
+
...over,
|
|
14
|
+
});
|
|
15
|
+
const baseline = (routes) => ({
|
|
16
|
+
version: 1,
|
|
17
|
+
capturedAt: '2026-07-06T00:00:00Z',
|
|
18
|
+
engine: 'dev-server',
|
|
19
|
+
framework: 'next',
|
|
20
|
+
command: 'npm run dev',
|
|
21
|
+
routes: Object.fromEntries(Object.entries(routes).map(([route, sig]) => [route, { signature: sig }])),
|
|
22
|
+
});
|
|
23
|
+
describe('compareBaselines', () => {
|
|
24
|
+
it('reports clean when identical', () => {
|
|
25
|
+
const result = compareBaselines(baseline({ '/': signature() }), baseline({ '/': signature() }));
|
|
26
|
+
expect(result).toMatchObject({ clean: 1, drifted: 0, unverified: 0 });
|
|
27
|
+
});
|
|
28
|
+
it('reports a single text change with index and before/after', () => {
|
|
29
|
+
const after = signature();
|
|
30
|
+
after.blocks = after.blocks.map((block, i) => (i === 2 ? { ...block, text: 'Get access' } : block));
|
|
31
|
+
const { diffs, drifted } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
32
|
+
expect(drifted).toBe(1);
|
|
33
|
+
expect(diffs[0].blocks).toEqual([
|
|
34
|
+
{ kind: 'text-changed', index: 2, tag: 'p', before: 'Get early access', after: 'Get access' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it('reports one block-added without cascading through LCS', () => {
|
|
38
|
+
const after = signature();
|
|
39
|
+
after.blocks = [after.blocks[0], { tag: 'p', text: 'Injected', marks: [] }, ...after.blocks.slice(1)];
|
|
40
|
+
const { diffs } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
41
|
+
expect(diffs[0].blocks).toEqual([{ kind: 'block-added', index: 1, tag: 'p', after: 'Injected' }]);
|
|
42
|
+
});
|
|
43
|
+
it('reports lost marks even when text is unchanged', () => {
|
|
44
|
+
const after = signature();
|
|
45
|
+
after.blocks = after.blocks.map((block, i) => (i === 1 ? { ...block, marks: [] } : block));
|
|
46
|
+
const { diffs } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
47
|
+
expect(diffs[0].blocks[0]).toMatchObject({ kind: 'marks-changed', index: 1 });
|
|
48
|
+
expect(formatRouteDiff(diffs[0])[0]).toContain('marks changed at block 1');
|
|
49
|
+
});
|
|
50
|
+
it('reports status change and media change', () => {
|
|
51
|
+
const after = signature({ status: 500, images: ['/images/other.png'] });
|
|
52
|
+
const { diffs } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
53
|
+
expect(diffs[0].statusChange).toEqual({ before: 200, after: 500 });
|
|
54
|
+
expect(diffs[0].images[0]).toMatchObject({ kind: 'changed', before: '/images/hero.png' });
|
|
55
|
+
});
|
|
56
|
+
it('treats rehosted media as an informational note, not drift', () => {
|
|
57
|
+
const after = signature({ images: ['https://assets.fimo.ai/uploads/hero-abc123.png'] });
|
|
58
|
+
const { diffs, drifted } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
59
|
+
expect(drifted).toBe(0);
|
|
60
|
+
expect(diffs[0].notes[0]).toContain('media rehosted');
|
|
61
|
+
});
|
|
62
|
+
it('counts skipped routes as unverified', () => {
|
|
63
|
+
const base = baseline({ '/': signature() });
|
|
64
|
+
base.routes['/blog/:slug'] = { skipped: 'dynamic-params' };
|
|
65
|
+
const current = baseline({ '/': signature() });
|
|
66
|
+
current.routes['/blog/:slug'] = { skipped: 'dynamic-params' };
|
|
67
|
+
const result = compareBaselines(base, current);
|
|
68
|
+
expect(result).toMatchObject({ clean: 1, unverified: 1 });
|
|
69
|
+
});
|
|
70
|
+
it('reports link href changes', () => {
|
|
71
|
+
const after = signature({ links: [{ href: '/wp.pdf', text: 'PDF' }] });
|
|
72
|
+
const { diffs } = compareBaselines(baseline({ '/': signature() }), baseline({ '/': after }));
|
|
73
|
+
expect(diffs[0].links[0]).toMatchObject({ kind: 'changed', before: '/whitepaper.pdf', after: '/wp.pdf' });
|
|
74
|
+
});
|
|
75
|
+
it('treats by-design dynamic text as equal when both sides match a parityIgnore pattern', () => {
|
|
76
|
+
const clockBefore = signature({ blocks: [{ tag: 'div', text: '4:50:25 PM CET', marks: [] }] });
|
|
77
|
+
const clockAfter = signature({ blocks: [{ tag: 'div', text: '5:04:30 PM CET', marks: [] }] });
|
|
78
|
+
const pattern = String.raw `\d{1,2}:\d{2}:\d{2} [AP]M [A-Z]+`;
|
|
79
|
+
const without = compareBaselines(baseline({ '/': clockBefore }), baseline({ '/': clockAfter }));
|
|
80
|
+
expect(without.drifted).toBe(1);
|
|
81
|
+
const withIgnore = compareBaselines(baseline({ '/': clockBefore }), baseline({ '/': clockAfter }), {
|
|
82
|
+
ignoreTextPatterns: [pattern],
|
|
83
|
+
});
|
|
84
|
+
expect(withIgnore).toMatchObject({ drifted: 0, clean: 1 });
|
|
85
|
+
// A REAL edit does not hide behind the pattern: only one side matches.
|
|
86
|
+
const edited = signature({ blocks: [{ tag: 'div', text: 'Totally different copy', marks: [] }] });
|
|
87
|
+
const realDrift = compareBaselines(baseline({ '/': clockBefore }), baseline({ '/': edited }), {
|
|
88
|
+
ignoreTextPatterns: [pattern],
|
|
89
|
+
});
|
|
90
|
+
expect(realDrift.drifted).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
it('ignores invalid parityIgnore regexes instead of crashing', () => {
|
|
93
|
+
const result = compareBaselines(baseline({ '/': signature() }), baseline({ '/': signature() }), {
|
|
94
|
+
ignoreTextPatterns: ['[unclosed'],
|
|
95
|
+
});
|
|
96
|
+
expect(result.clean).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RouteSignature } from './types.js';
|
|
2
|
+
/** Strip the capture origin so differing ports between runs compare equal. */
|
|
3
|
+
export declare function normalizeHref(href: string, origin: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Media src normalization: origin strip + Next image-optimizer decode, so a
|
|
6
|
+
* pre-wrap `next/image` (`/_next/image?url=…&w=…`) and a post-wrap plain
|
|
7
|
+
* `<img src>` of the same asset compare equal.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeMediaSrc(src: string, origin: string): string;
|
|
10
|
+
export declare function extractRouteSignature(html: string, opts: {
|
|
11
|
+
origin: string;
|
|
12
|
+
status: number;
|
|
13
|
+
}): RouteSignature;
|