@madojs/mado 0.10.1 → 0.11.1
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/AGENTS.md +24 -26
- package/CHANGELOG.md +95 -0
- package/README.md +22 -47
- package/TODO.md +52 -48
- package/dist/src/component.d.ts +2 -1
- package/dist/src/component.js +5 -2
- package/dist/src/component.js.map +1 -1
- package/dist/src/each.d.ts +1 -1
- package/dist/src/each.js +1 -1
- package/dist/src/each.js.map +1 -1
- package/dist/src/html/bindings.js +3 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/index.d.ts +11 -6
- package/dist/src/index.js +5 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/lazy.d.ts +1 -1
- package/dist/src/lazy.js +1 -1
- package/dist/src/lazy.js.map +1 -1
- package/dist/src/page.d.ts +17 -21
- package/dist/src/page.js +7 -12
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +1 -1
- package/dist/src/router/manifest.js +21 -13
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +2 -2
- package/dist/src/router/match.js +3 -3
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +1 -1
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/vite/index.d.ts +10 -0
- package/dist/src/vite/index.js +33 -0
- package/dist/src/vite/index.js.map +1 -0
- package/docs/en/00-the-mado-way.md +25 -12
- package/docs/en/01-routing.md +90 -142
- package/docs/en/02-project-layout.md +59 -53
- package/docs/en/03-static-bake.md +5 -6
- package/docs/en/05-why-mado.md +6 -6
- package/docs/en/06-for-backenders.md +18 -22
- package/docs/en/08-llm-zero-history-test.md +9 -14
- package/docs/en/09-shadow-vs-light-dom.md +28 -36
- package/docs/en/10-app-architecture.md +158 -96
- package/docs/en/11-layouts.md +22 -24
- package/docs/en/12-auth-and-api.md +89 -182
- package/docs/en/13-deployment.md +18 -22
- package/docs/en/14-testing.md +4 -4
- package/docs/en/16-bake-cookbook.md +11 -12
- package/docs/en/18-api-freeze-map.md +6 -4
- package/docs/en/20-v1-stability.md +1 -1
- package/docs/fr/00-the-mado-way.md +55 -90
- package/docs/fr/01-routing.md +70 -152
- package/docs/fr/02-project-layout.md +61 -42
- package/docs/fr/03-static-bake.md +1 -1
- package/docs/fr/05-why-mado.md +6 -6
- package/docs/fr/06-for-backenders.md +7 -7
- package/docs/fr/08-llm-zero-history-test.md +21 -48
- package/docs/fr/09-shadow-vs-light-dom.md +43 -162
- package/docs/fr/10-app-architecture.md +110 -33
- package/docs/fr/11-layouts.md +24 -12
- package/docs/fr/12-auth-and-api.md +63 -22
- package/docs/fr/13-deployment.md +7 -10
- package/docs/fr/14-testing.md +1 -1
- package/docs/fr/16-bake-cookbook.md +2 -2
- package/docs/fr/18-api-freeze-map.md +1 -1
- package/docs/fr/20-v1-stability.md +1 -1
- package/docs/recipes/nginx/README.md +13 -0
- package/docs/ru/00-the-mado-way.md +53 -75
- package/docs/ru/01-routing.md +68 -143
- package/docs/ru/02-project-layout.md +61 -41
- package/docs/ru/03-static-bake.md +2 -2
- package/docs/ru/05-why-mado.md +6 -6
- package/docs/ru/06-for-backenders.md +7 -7
- package/docs/ru/08-llm-zero-history-test.md +9 -14
- package/docs/ru/09-shadow-vs-light-dom.md +43 -178
- package/docs/ru/10-app-architecture.md +115 -63
- package/docs/ru/11-layouts.md +24 -24
- package/docs/ru/12-auth-and-api.md +57 -35
- package/docs/ru/13-deployment.md +7 -11
- package/docs/ru/14-testing.md +1 -1
- package/docs/ru/16-bake-cookbook.md +12 -6
- package/docs/ru/18-api-freeze-map.md +5 -3
- package/docs/ru/20-v1-stability.md +1 -1
- package/docs/uk/00-the-mado-way.md +70 -44
- package/docs/uk/01-routing.md +41 -47
- package/docs/uk/02-project-layout.md +68 -41
- package/docs/uk/03-static-bake.md +1 -2
- package/docs/uk/06-for-backenders.md +3 -3
- package/docs/uk/08-llm-zero-history-test.md +22 -24
- package/docs/uk/09-shadow-vs-light-dom.md +37 -86
- package/docs/uk/10-app-architecture.md +72 -31
- package/docs/uk/11-layouts.md +25 -12
- package/docs/uk/12-auth-and-api.md +58 -22
- package/docs/uk/13-deployment.md +4 -3
- package/docs/uk/14-testing.md +1 -1
- package/docs/uk/18-api-freeze-map.md +1 -1
- package/docs/uk/20-v1-stability.md +1 -1
- package/llms.txt +14 -15
- package/package.json +18 -11
- package/scripts/_config.mjs +15 -161
- package/scripts/bake.mjs +74 -63
- package/scripts/cli/generate.mjs +348 -0
- package/scripts/cli/help.mjs +27 -0
- package/scripts/cli/index.mjs +79 -0
- package/scripts/cli/init.mjs +153 -0
- package/scripts/cli/release.mjs +152 -0
- package/scripts/cli/run.mjs +96 -0
- package/scripts/cli.mjs +2 -621
- package/scripts/package-smoke.mjs +4 -1
- package/scripts/preview.mjs +13 -37
- package/scripts/size-budget.mjs +5 -2
- package/scripts/vite.default.mjs +11 -0
- package/starters/default/.editorconfig +12 -0
- package/starters/default/README.md +74 -0
- package/starters/default/eslint.config.mjs +256 -0
- package/starters/default/index.html +13 -0
- package/starters/default/package.json +30 -0
- package/starters/default/public/favicon.svg +4 -0
- package/starters/default/src/app.routes.ts +39 -0
- package/starters/default/src/layouts/app-shell.layout.ts +35 -0
- package/starters/default/src/layouts/auth-shell.layout.ts +17 -0
- package/starters/default/src/main.ts +16 -0
- package/starters/default/src/modules/auth/_contracts/auth-api.types.ts +17 -0
- package/starters/default/src/modules/auth/auth.connector.ts +45 -0
- package/starters/default/src/modules/auth/auth.guard.ts +22 -0
- package/starters/default/src/modules/auth/auth.public.ts +9 -0
- package/starters/default/src/modules/auth/auth.routes.ts +8 -0
- package/starters/default/src/modules/auth/auth.service.ts +71 -0
- package/starters/default/src/modules/auth/auth.types.ts +15 -0
- package/starters/default/src/modules/auth/login.page.ts +62 -0
- package/starters/default/src/modules/billing/_contracts/stripe.types.ts +17 -0
- package/starters/default/src/modules/billing/api/stripe.connector.ts +71 -0
- package/starters/default/src/modules/billing/billing.public.ts +5 -0
- package/starters/default/src/modules/billing/billing.routes.ts +9 -0
- package/starters/default/src/modules/billing/billing.types.ts +15 -0
- package/starters/default/src/modules/billing/components/invoice-status-badge.component.ts +43 -0
- package/starters/default/src/modules/billing/data/invoices.resource.ts +35 -0
- package/starters/default/src/modules/billing/pages/invoice-detail.page.ts +70 -0
- package/starters/default/src/modules/billing/pages/invoices-list.page.ts +73 -0
- package/starters/default/src/modules/home/home.page.ts +34 -0
- package/starters/default/src/modules/home/not-found.page.ts +11 -0
- package/starters/default/src/shared/http/http-client.ts +86 -0
- package/starters/default/src/shared/http/http-error.ts +37 -0
- package/starters/default/src/shared/http/interceptors.ts +59 -0
- package/starters/default/src/shared/lib/format-date.ts +19 -0
- package/starters/default/src/shared/styles/content.css +70 -0
- package/starters/default/src/shared/styles/reset.css +32 -0
- package/starters/default/src/shared/styles/shell.css +57 -0
- package/starters/default/src/shared/styles/tokens.css +44 -0
- package/starters/default/src/shared/ui/x-button.component.ts +49 -0
- package/starters/default/src/shared/ui/x-spinner.component.ts +22 -0
- package/starters/default/src/styles.d.ts +1 -0
- package/starters/default/src/vite-env.d.ts +1 -0
- package/starters/default/tsconfig.json +24 -0
- package/starters/default/vite.config.ts +9 -0
- package/MADO_V1_PLAN.md +0 -179
- package/ROADMAP.md +0 -178
- package/dist/src/html.d.ts +0 -18
- package/dist/src/html.js +0 -17
- package/dist/src/html.js.map +0 -1
- package/dist/src/router.d.ts +0 -13
- package/dist/src/router.js +0 -13
- package/dist/src/router.js.map +0 -1
- package/scripts/bundle.mjs +0 -212
- package/scripts/llm-zero-history-smoke.mjs +0 -93
- package/scripts/new.mjs +0 -80
- package/scripts/showcase-regression.mjs +0 -392
- package/server/serve.mjs +0 -455
- package/starters/admin/README.md +0 -63
- package/starters/admin/index.html +0 -28
- package/starters/admin/mado.config.json +0 -22
- package/starters/admin/package.json +0 -24
- package/starters/admin/public/favicon.svg +0 -4
- package/starters/admin/src/components/x-button.ts +0 -82
- package/starters/admin/src/components/x-input.ts +0 -105
- package/starters/admin/src/layouts/app.ts +0 -101
- package/starters/admin/src/layouts/auth.ts +0 -41
- package/starters/admin/src/lib/api.ts +0 -184
- package/starters/admin/src/lib/auth.ts +0 -83
- package/starters/admin/src/main.ts +0 -15
- package/starters/admin/src/pages/admin/dashboard.ts +0 -48
- package/starters/admin/src/pages/admin/order-detail.ts +0 -80
- package/starters/admin/src/pages/admin/orders.ts +0 -117
- package/starters/admin/src/pages/home.ts +0 -34
- package/starters/admin/src/pages/login.ts +0 -70
- package/starters/admin/src/pages/not-found.ts +0 -12
- package/starters/admin/src/routes.ts +0 -40
- package/starters/admin/src/styles/global.ts +0 -86
- package/starters/admin/tsconfig.json +0 -15
- package/starters/crud/README.md +0 -33
- package/starters/crud/index.html +0 -28
- package/starters/crud/mado.config.json +0 -20
- package/starters/crud/package.json +0 -24
- package/starters/crud/src/components/app-shell.ts +0 -56
- package/starters/crud/src/components/ticket-detail.ts +0 -33
- package/starters/crud/src/components/ticket-form.ts +0 -69
- package/starters/crud/src/components/ticket-list.ts +0 -66
- package/starters/crud/src/lib/api.ts +0 -76
- package/starters/crud/src/main.ts +0 -9
- package/starters/crud/src/pages/home.ts +0 -34
- package/starters/crud/src/pages/not-found.ts +0 -12
- package/starters/crud/src/pages/ticket-detail.ts +0 -7
- package/starters/crud/src/pages/ticket-new.ts +0 -7
- package/starters/crud/src/pages/tickets.ts +0 -7
- package/starters/crud/src/routes.ts +0 -11
- package/starters/crud/src/styles/global.ts +0 -155
- package/starters/crud/tsconfig.json +0 -15
- package/starters/minimal/README.md +0 -21
- package/starters/minimal/index.html +0 -28
- package/starters/minimal/mado.config.json +0 -20
- package/starters/minimal/package.json +0 -24
- package/starters/minimal/src/components/app-counter.ts +0 -31
- package/starters/minimal/src/main.ts +0 -9
- package/starters/minimal/src/pages/home.ts +0 -35
- package/starters/minimal/src/pages/not-found.ts +0 -14
- package/starters/minimal/src/routes.ts +0 -8
- package/starters/minimal/src/styles/global.ts +0 -60
- package/starters/minimal/tsconfig.json +0 -15
- package/templates/page-detail.ts +0 -63
- package/templates/page-form.ts +0 -94
- package/templates/page-list.ts +0 -79
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
|
|
8
|
-
const exec = promisify(execFile);
|
|
9
|
-
const root = process.cwd();
|
|
10
|
-
const ticketsDir = join(root, "examples", "tickets");
|
|
11
|
-
|
|
12
|
-
const llms = await read("llms.txt");
|
|
13
|
-
assertIncludes(llms, "This is NOT React", "llms.txt must keep the React warning");
|
|
14
|
-
assertIncludes(llms, "Canonical CRUD pattern", "llms.txt must keep the CRUD recipe");
|
|
15
|
-
assertIncludes(llms, "resource()", "llms.txt must document resource()");
|
|
16
|
-
assertIncludes(llms, "mutation", "llms.txt must document mutation()");
|
|
17
|
-
assertIncludes(llms, "useForm", "llms.txt must document useForm()");
|
|
18
|
-
|
|
19
|
-
const files = await collectTs(ticketsDir);
|
|
20
|
-
const code = (await Promise.all(files.map((file) => read(file)))).join("\n");
|
|
21
|
-
const routes = await read(join(ticketsDir, "routes.ts"));
|
|
22
|
-
|
|
23
|
-
for (const route of ['"/"', '"/tickets"', '"/tickets/new"', '"/tickets/:id"', '"*"']) {
|
|
24
|
-
assertIncludes(routes, route, `tickets routes must include ${route}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
for (const api of [
|
|
28
|
-
"component(",
|
|
29
|
-
"html`",
|
|
30
|
-
"signal(",
|
|
31
|
-
"computed(",
|
|
32
|
-
"resource(",
|
|
33
|
-
"mutation(",
|
|
34
|
-
"invalidates",
|
|
35
|
-
"queryParam(",
|
|
36
|
-
"each(",
|
|
37
|
-
"useForm(",
|
|
38
|
-
]) {
|
|
39
|
-
assertIncludes(code, api, `tickets example must exercise ${api}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const forbidden = [
|
|
43
|
-
/\buseState\s*\(/,
|
|
44
|
-
/\buseEffect\s*\(/,
|
|
45
|
-
/\$state\b/,
|
|
46
|
-
/\bref\s*\(/,
|
|
47
|
-
/from\s+["']react["']/,
|
|
48
|
-
/class\s+\w+\s+extends\s+HTMLElement/,
|
|
49
|
-
/<>\s*$/,
|
|
50
|
-
/(^|[^?.\w-])disabled=\$\{/,
|
|
51
|
-
/(^|[^?.\w-])checked=\$\{/,
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
for (const pattern of forbidden) {
|
|
55
|
-
if (pattern.test(code)) {
|
|
56
|
-
throw new Error(`[llm-smoke] forbidden generated pattern: ${pattern}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await run(process.execPath, ["scripts/cli.mjs", "build"]);
|
|
61
|
-
await run(process.execPath, ["--test", "test/tickets-smoke.test.mjs"]);
|
|
62
|
-
|
|
63
|
-
console.log("[llm-smoke] ok examples/tickets follows llms.txt and passes smoke");
|
|
64
|
-
|
|
65
|
-
async function collectTs(dir) {
|
|
66
|
-
const out = [];
|
|
67
|
-
for (const entry of await readdir(dir)) {
|
|
68
|
-
const file = join(dir, entry);
|
|
69
|
-
const s = await stat(file);
|
|
70
|
-
if (s.isDirectory()) out.push(...await collectTs(file));
|
|
71
|
-
else if (file.endsWith(".ts")) out.push(file);
|
|
72
|
-
}
|
|
73
|
-
return out.sort();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function read(file) {
|
|
77
|
-
return readFile(file, "utf8");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function assertIncludes(text, needle, message) {
|
|
81
|
-
if (!text.includes(needle)) throw new Error(`[llm-smoke] ${message}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function run(cmd, args) {
|
|
85
|
-
console.log(`[llm-smoke] ${cmd} ${args.join(" ")}`);
|
|
86
|
-
try {
|
|
87
|
-
await exec(cmd, args, { cwd: root, maxBuffer: 20 * 1024 * 1024 });
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (err.stdout) process.stdout.write(err.stdout);
|
|
90
|
-
if (err.stderr) process.stderr.write(err.stderr);
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
93
|
-
}
|
package/scripts/new.mjs
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// Scaffold a new page from templates/.
|
|
2
|
-
//
|
|
3
|
-
// node scripts/new.mjs list users
|
|
4
|
-
// node scripts/new.mjs form sign-up
|
|
5
|
-
// node scripts/new.mjs detail post
|
|
6
|
-
//
|
|
7
|
-
// Result: examples/pages/<name>.ts (or src/pages/, when present)
|
|
8
|
-
// with __name__ / __Name__ placeholders replaced.
|
|
9
|
-
//
|
|
10
|
-
// Zero runtime dependencies; generated apps use dev tooling only.
|
|
11
|
-
|
|
12
|
-
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
13
|
-
import { dirname, join, resolve } from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
|
|
16
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
-
const PROJECT_ROOT = resolve(process.cwd());
|
|
18
|
-
|
|
19
|
-
const [, , kind, rawName] = process.argv;
|
|
20
|
-
|
|
21
|
-
if (!kind || !rawName) {
|
|
22
|
-
console.error("usage: node scripts/new.mjs <list|form|detail> <name>");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const templates = {
|
|
27
|
-
list: "templates/page-list.ts",
|
|
28
|
-
form: "templates/page-form.ts",
|
|
29
|
-
detail: "templates/page-detail.ts",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const tplPath = templates[kind];
|
|
33
|
-
if (!tplPath) {
|
|
34
|
-
console.error(`unknown template: ${kind} (available: list, form, detail)`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// name → kebab-case (for file names and tags)
|
|
39
|
-
const kebab = rawName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
40
|
-
// Name → PascalCase for titles
|
|
41
|
-
const pascal = kebab
|
|
42
|
-
.split("-")
|
|
43
|
-
.filter(Boolean)
|
|
44
|
-
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
45
|
-
.join("");
|
|
46
|
-
|
|
47
|
-
const targetDir = (await exists("src/pages"))
|
|
48
|
-
? "src/pages"
|
|
49
|
-
: (await exists("examples/basic/pages"))
|
|
50
|
-
? "examples/basic/pages"
|
|
51
|
-
: "src/pages";
|
|
52
|
-
await mkdir(targetDir, { recursive: true });
|
|
53
|
-
|
|
54
|
-
const targetFile = join(
|
|
55
|
-
targetDir,
|
|
56
|
-
kind === "detail" ? `${kebab}-detail.ts` : `${kebab}.ts`,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
if (await exists(targetFile)) {
|
|
60
|
-
console.error(`already exists: ${targetFile}`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const src = await readFile(join(PACKAGE_ROOT, tplPath), "utf8");
|
|
65
|
-
const out = src.replaceAll("__name__", kebab).replaceAll("__Name__", pascal);
|
|
66
|
-
|
|
67
|
-
await writeFile(targetFile, out);
|
|
68
|
-
console.log(`✓ created: ${targetFile}`);
|
|
69
|
-
console.log(` remember to add this to routes.ts:`);
|
|
70
|
-
const routePath = kind === "detail" ? `/${kebab}/:id` : `/${kebab}`;
|
|
71
|
-
console.log(` '${routePath}': () => import('./pages/${kebab}${kind === "detail" ? "-detail" : ""}.js'),`);
|
|
72
|
-
|
|
73
|
-
async function exists(p) {
|
|
74
|
-
try {
|
|
75
|
-
await access(join(PROJECT_ROOT, p));
|
|
76
|
-
return true;
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
// Browser regression for examples/showcase.
|
|
2
|
-
//
|
|
3
|
-
// Optional by design: regular `npm test` stays Node/linkedom-only. Run this
|
|
4
|
-
// when you want real browser confidence for routing, forms and Shadow DOM.
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { setTimeout as wait } from "node:timers/promises";
|
|
12
|
-
|
|
13
|
-
const PORT = Number(process.env.PORT ?? 5181);
|
|
14
|
-
const BASE = `http://localhost:${PORT}`;
|
|
15
|
-
|
|
16
|
-
async function loadPlaywright() {
|
|
17
|
-
try {
|
|
18
|
-
return await import("playwright-core");
|
|
19
|
-
} catch {
|
|
20
|
-
console.warn("[showcase-regression] playwright-core is not installed; skipped.");
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function waitForServer() {
|
|
26
|
-
for (let i = 0; i < 60; i++) {
|
|
27
|
-
try {
|
|
28
|
-
const res = await fetch(BASE);
|
|
29
|
-
if (res.ok) return;
|
|
30
|
-
} catch {
|
|
31
|
-
/* server not ready */
|
|
32
|
-
}
|
|
33
|
-
await wait(100);
|
|
34
|
-
}
|
|
35
|
-
throw new Error(`Server did not start at ${BASE}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const server = spawn("node", ["server/serve.mjs"], {
|
|
39
|
-
env: {
|
|
40
|
-
...process.env,
|
|
41
|
-
PORT: String(PORT),
|
|
42
|
-
EXAMPLE: "showcase",
|
|
43
|
-
NO_HMR: "1",
|
|
44
|
-
},
|
|
45
|
-
stdio: "pipe",
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
let browser;
|
|
49
|
-
let chrome;
|
|
50
|
-
let tmpProfile;
|
|
51
|
-
try {
|
|
52
|
-
await waitForServer();
|
|
53
|
-
const pw = await loadPlaywright();
|
|
54
|
-
if (pw) {
|
|
55
|
-
try {
|
|
56
|
-
browser = await pw.chromium.launch();
|
|
57
|
-
const page = await browser.newPage();
|
|
58
|
-
await runWithPage(page);
|
|
59
|
-
await page.close();
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.warn(
|
|
62
|
-
`[showcase-regression] Playwright Chromium could not start; falling back to system Chrome. ${error.message}`,
|
|
63
|
-
);
|
|
64
|
-
if (browser) await browser.close();
|
|
65
|
-
browser = undefined;
|
|
66
|
-
const result = await runWithChromeCdp();
|
|
67
|
-
chrome = result.chrome;
|
|
68
|
-
tmpProfile = result.tmpProfile;
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
const result = await runWithChromeCdp();
|
|
72
|
-
chrome = result.chrome;
|
|
73
|
-
tmpProfile = result.tmpProfile;
|
|
74
|
-
}
|
|
75
|
-
console.log("[showcase-regression] passed");
|
|
76
|
-
} finally {
|
|
77
|
-
if (browser) await browser.close();
|
|
78
|
-
if (chrome) {
|
|
79
|
-
chrome.kill("SIGTERM");
|
|
80
|
-
await wait(300);
|
|
81
|
-
}
|
|
82
|
-
if (tmpProfile) {
|
|
83
|
-
try {
|
|
84
|
-
await rm(tmpProfile, { recursive: true, force: true, maxRetries: 3, retryDelay: 120 });
|
|
85
|
-
} catch {
|
|
86
|
-
/* best-effort cleanup */
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
server.kill("SIGTERM");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function expectSingle(page, selector) {
|
|
93
|
-
const count = await page.locator(`#app ${selector}`).count();
|
|
94
|
-
assert.equal(count, 1, `${selector} should be the only active page host`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function runWithPage(page) {
|
|
98
|
-
await page.goto(BASE);
|
|
99
|
-
await page.waitForSelector("x-hero");
|
|
100
|
-
assert.equal(await page.locator("#app x-hero").count(), 1);
|
|
101
|
-
|
|
102
|
-
await page.click("x-nav >>> a[href='/app/login']");
|
|
103
|
-
await page.waitForSelector("x-login");
|
|
104
|
-
await page.fill("x-login >>> input[name='email']", "anna@example.com");
|
|
105
|
-
await page.fill("x-login >>> input[name='password']", "demo");
|
|
106
|
-
await page.click("x-login >>> button[type='submit']");
|
|
107
|
-
await page.waitForSelector("x-dashboard");
|
|
108
|
-
await expectSingle(page, "x-dashboard");
|
|
109
|
-
|
|
110
|
-
await page.click("x-app-layout a[href='/app/accounts']");
|
|
111
|
-
await page.waitForSelector("x-accounts-list");
|
|
112
|
-
await page.fill("x-accounts-list >>> input[type='search']", "Northwind");
|
|
113
|
-
await page.waitForTimeout(350);
|
|
114
|
-
await expectSingle(page, "x-accounts-list");
|
|
115
|
-
|
|
116
|
-
await page.click("x-accounts-list >>> a[href='/app/accounts/new']");
|
|
117
|
-
await page.waitForSelector("x-account-new");
|
|
118
|
-
await page.fill("x-account-new >>> input[name='name']", "Browser Test Co");
|
|
119
|
-
await page.fill("x-account-new >>> input[name='domain']", "browser.example");
|
|
120
|
-
await page.fill("x-account-new >>> input[name='mrr']", "6200");
|
|
121
|
-
await page.fill("x-account-new >>> textarea[name='notes']", "Created by browser regression.");
|
|
122
|
-
await page.click("x-account-new >>> button[type='submit']");
|
|
123
|
-
await page.waitForSelector("x-account-detail");
|
|
124
|
-
await expectSingle(page, "x-account-detail");
|
|
125
|
-
|
|
126
|
-
await page.click("x-account-detail >>> button:has-text('New deal')");
|
|
127
|
-
await page.fill("x-account-detail >>> input[name='title']", "Browser pipeline");
|
|
128
|
-
await page.fill("x-account-detail >>> input[name='value']", "88000");
|
|
129
|
-
await page.fill("x-account-detail >>> textarea[name='notes']", "Created through modal regression.");
|
|
130
|
-
await page.click("x-account-detail >>> button:has-text('Create deal')");
|
|
131
|
-
await page.waitForTimeout(500);
|
|
132
|
-
await expectSingle(page, "x-account-detail");
|
|
133
|
-
|
|
134
|
-
await page.click("x-app-layout a[href='/app/deals']");
|
|
135
|
-
await page.waitForSelector("x-deals-list");
|
|
136
|
-
await expectSingle(page, "x-deals-list");
|
|
137
|
-
|
|
138
|
-
const activeAppHosts = await page.locator(
|
|
139
|
-
"#app x-dashboard,#app x-accounts-list,#app x-account-new,#app x-account-detail,#app x-deals-list,#app x-deal-detail,#app x-settings,#app x-login",
|
|
140
|
-
).count();
|
|
141
|
-
assert.equal(activeAppHosts, 1);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function runWithChromeCdp() {
|
|
145
|
-
if (typeof WebSocket === "undefined") {
|
|
146
|
-
console.warn("[showcase-regression] no WebSocket for CDP fallback; skipped.");
|
|
147
|
-
process.exit(0);
|
|
148
|
-
}
|
|
149
|
-
const debugPort = Number(process.env.CHROME_DEBUG_PORT ?? 9228);
|
|
150
|
-
const chromeBin = process.env.CHROME_BIN ?? "google-chrome";
|
|
151
|
-
const profile = await mkdtemp(join(tmpdir(), "mado-showcase-chrome-"));
|
|
152
|
-
const chromeProc = spawn(
|
|
153
|
-
chromeBin,
|
|
154
|
-
[
|
|
155
|
-
"--headless=new",
|
|
156
|
-
"--disable-gpu",
|
|
157
|
-
"--no-sandbox",
|
|
158
|
-
"--disable-dev-shm-usage",
|
|
159
|
-
`--remote-debugging-port=${debugPort}`,
|
|
160
|
-
`--user-data-dir=${profile}`,
|
|
161
|
-
BASE,
|
|
162
|
-
],
|
|
163
|
-
{ stdio: "ignore" },
|
|
164
|
-
);
|
|
165
|
-
chromeProc.on("error", () => {
|
|
166
|
-
console.warn("[showcase-regression] google-chrome is not available; skipped.");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const wsUrl = await waitForCdp(debugPort);
|
|
170
|
-
if (!wsUrl) {
|
|
171
|
-
chromeProc.kill("SIGTERM");
|
|
172
|
-
await rm(profile, { recursive: true, force: true });
|
|
173
|
-
console.warn("[showcase-regression] Chrome CDP did not start; skipped.");
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const cdp = await connectCdp(wsUrl);
|
|
178
|
-
await cdp.send("Runtime.enable");
|
|
179
|
-
await cdp.send("Page.enable");
|
|
180
|
-
await cdp.send("Page.navigate", { url: BASE });
|
|
181
|
-
await waitForCdpSelector(cdp, "x-hero");
|
|
182
|
-
assert.equal(await cdpCount(cdp, "x-hero"), 1);
|
|
183
|
-
|
|
184
|
-
await cdpClick(cdp, "x-nav >>> a[href='/app/login']");
|
|
185
|
-
await waitForCdpSelector(cdp, "x-login");
|
|
186
|
-
await cdpFill(cdp, "x-login >>> input[name='email']", "anna@example.com");
|
|
187
|
-
await cdpFill(cdp, "x-login >>> input[name='password']", "demo");
|
|
188
|
-
await cdpClick(cdp, "x-login >>> button[type='submit']");
|
|
189
|
-
await waitForCdpSelector(cdp, "x-dashboard");
|
|
190
|
-
assert.equal(await cdpCount(cdp, "x-dashboard"), 1);
|
|
191
|
-
|
|
192
|
-
await cdpClick(cdp, "x-app-layout >>> a[href='/app/accounts']");
|
|
193
|
-
await waitForCdpSelector(cdp, "x-accounts-list");
|
|
194
|
-
await cdpFill(cdp, "x-accounts-list >>> input[type='search']", "Northwind");
|
|
195
|
-
await wait(350);
|
|
196
|
-
assert.equal(await cdpCount(cdp, "x-accounts-list"), 1);
|
|
197
|
-
|
|
198
|
-
await cdpClick(cdp, "x-accounts-list >>> a[href='/app/accounts/new']");
|
|
199
|
-
await waitForCdpSelector(cdp, "x-account-new");
|
|
200
|
-
await cdpFill(cdp, "x-account-new >>> input[name='name']", "Browser Test Co");
|
|
201
|
-
await cdpFill(cdp, "x-account-new >>> input[name='domain']", "browser.example");
|
|
202
|
-
await cdpFill(cdp, "x-account-new >>> input[name='mrr']", "6200");
|
|
203
|
-
await cdpFill(cdp, "x-account-new >>> textarea[name='notes']", "Created by browser regression.");
|
|
204
|
-
await cdpClick(cdp, "x-account-new >>> button[type='submit']");
|
|
205
|
-
await waitForCdpSelector(cdp, "x-account-detail");
|
|
206
|
-
assert.equal(await cdpCount(cdp, "x-account-detail"), 1);
|
|
207
|
-
|
|
208
|
-
await cdpClickText(cdp, "x-account-detail >>> button", "New deal");
|
|
209
|
-
await cdpFill(cdp, "x-account-detail >>> input[name='title']", "Browser pipeline");
|
|
210
|
-
await cdpFill(cdp, "x-account-detail >>> input[name='value']", "88000");
|
|
211
|
-
await cdpFill(cdp, "x-account-detail >>> textarea[name='notes']", "Created through modal regression.");
|
|
212
|
-
await cdpClickText(cdp, "x-account-detail >>> button", "Create deal");
|
|
213
|
-
await wait(500);
|
|
214
|
-
assert.equal(await cdpCount(cdp, "x-account-detail"), 1);
|
|
215
|
-
|
|
216
|
-
await cdpClick(cdp, "x-app-layout >>> a[href='/app/deals']");
|
|
217
|
-
await waitForCdpSelector(cdp, "x-deals-list");
|
|
218
|
-
assert.equal(await cdpCount(cdp, "x-deals-list"), 1);
|
|
219
|
-
const active = await cdpCount(
|
|
220
|
-
cdp,
|
|
221
|
-
"x-dashboard,x-accounts-list,x-account-new,x-account-detail,x-deals-list,x-deal-detail,x-settings,x-login",
|
|
222
|
-
);
|
|
223
|
-
assert.equal(active, 1);
|
|
224
|
-
cdp.close();
|
|
225
|
-
return { chrome: chromeProc, tmpProfile: profile };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function waitForCdp(debugPort) {
|
|
229
|
-
for (let i = 0; i < 50; i++) {
|
|
230
|
-
try {
|
|
231
|
-
const list = await fetch(`http://127.0.0.1:${debugPort}/json/list`).then((r) => r.json());
|
|
232
|
-
for (const item of list) {
|
|
233
|
-
if (item.type === "page" && item.webSocketDebuggerUrl) return item.webSocketDebuggerUrl;
|
|
234
|
-
}
|
|
235
|
-
} catch {
|
|
236
|
-
/* not ready */
|
|
237
|
-
}
|
|
238
|
-
await wait(100);
|
|
239
|
-
}
|
|
240
|
-
return "";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function connectCdp(url) {
|
|
244
|
-
const ws = new WebSocket(url);
|
|
245
|
-
let seq = 1;
|
|
246
|
-
const pending = new Map();
|
|
247
|
-
ws.onmessage = (event) => {
|
|
248
|
-
const msg = JSON.parse(event.data);
|
|
249
|
-
if (!msg.id) return;
|
|
250
|
-
const entry = pending.get(msg.id);
|
|
251
|
-
if (!entry) return;
|
|
252
|
-
pending.delete(msg.id);
|
|
253
|
-
if (msg.error) entry.reject(new Error(msg.error.message));
|
|
254
|
-
else entry.resolve(msg.result);
|
|
255
|
-
};
|
|
256
|
-
return new Promise((resolve, reject) => {
|
|
257
|
-
ws.onerror = () => reject(new Error("CDP websocket failed"));
|
|
258
|
-
ws.onopen = () => {
|
|
259
|
-
resolve({
|
|
260
|
-
send(method, params = {}) {
|
|
261
|
-
const id = seq++;
|
|
262
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
263
|
-
return new Promise((res, rej) => pending.set(id, { resolve: res, reject: rej }));
|
|
264
|
-
},
|
|
265
|
-
close() {
|
|
266
|
-
ws.close();
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
};
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function helperScript() {
|
|
274
|
-
return `
|
|
275
|
-
(() => {
|
|
276
|
-
window.__deepFind = (selector, root = document) => {
|
|
277
|
-
const direct = root.querySelector(selector);
|
|
278
|
-
if (direct) return direct;
|
|
279
|
-
for (const el of root.querySelectorAll('*')) {
|
|
280
|
-
if (!el.shadowRoot) continue;
|
|
281
|
-
const found = window.__deepFind(selector, el.shadowRoot);
|
|
282
|
-
if (found) return found;
|
|
283
|
-
}
|
|
284
|
-
return null;
|
|
285
|
-
};
|
|
286
|
-
window.__deepCount = (selector, root = document) => {
|
|
287
|
-
let count = root.querySelectorAll(selector).length;
|
|
288
|
-
for (const el of root.querySelectorAll('*')) {
|
|
289
|
-
if (el.shadowRoot) count += window.__deepCount(selector, el.shadowRoot);
|
|
290
|
-
}
|
|
291
|
-
return count;
|
|
292
|
-
};
|
|
293
|
-
window.__pq = (path) => {
|
|
294
|
-
let root = document;
|
|
295
|
-
let node = null;
|
|
296
|
-
for (const raw of path.split('>>>')) {
|
|
297
|
-
const part = raw.trim();
|
|
298
|
-
node = root === document ? window.__deepFind(part, root) : root.querySelector(part);
|
|
299
|
-
if (!node) return null;
|
|
300
|
-
root = node.shadowRoot || node;
|
|
301
|
-
}
|
|
302
|
-
return node;
|
|
303
|
-
};
|
|
304
|
-
window.__pqAllCount = (selector) => window.__deepCount(selector);
|
|
305
|
-
})();
|
|
306
|
-
`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function cdpEval(cdp, expression) {
|
|
310
|
-
const result = await cdp.send("Runtime.evaluate", {
|
|
311
|
-
expression,
|
|
312
|
-
awaitPromise: true,
|
|
313
|
-
returnByValue: true,
|
|
314
|
-
});
|
|
315
|
-
if (result.exceptionDetails) {
|
|
316
|
-
const detail =
|
|
317
|
-
result.exceptionDetails.exception?.description ??
|
|
318
|
-
result.exceptionDetails.text ??
|
|
319
|
-
"Runtime.evaluate failed";
|
|
320
|
-
throw new Error(detail);
|
|
321
|
-
}
|
|
322
|
-
return result.result?.value;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function installHelper(cdp) {
|
|
326
|
-
await cdpEval(cdp, helperScript());
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function waitForCdpSelector(cdp, selector) {
|
|
330
|
-
await installHelper(cdp);
|
|
331
|
-
for (let i = 0; i < 80; i++) {
|
|
332
|
-
if (await cdpEval(cdp, `Boolean(window.__pq(${JSON.stringify(selector)}))`)) return;
|
|
333
|
-
await wait(100);
|
|
334
|
-
}
|
|
335
|
-
const snapshot = await cdpEval(
|
|
336
|
-
cdp,
|
|
337
|
-
`JSON.stringify({ href: location.href, title: document.title, body: document.body.innerHTML.slice(0, 500) })`,
|
|
338
|
-
);
|
|
339
|
-
throw new Error(`Selector not found: ${selector}; page=${snapshot}`);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function cdpClick(cdp, selector) {
|
|
343
|
-
await installHelper(cdp);
|
|
344
|
-
await cdpEval(cdp, `(() => {
|
|
345
|
-
const el = window.__pq(${JSON.stringify(selector)});
|
|
346
|
-
if (!el) throw new Error("missing ${selector}");
|
|
347
|
-
el.click();
|
|
348
|
-
return true;
|
|
349
|
-
})()`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function cdpClickText(cdp, selector, text) {
|
|
353
|
-
await installHelper(cdp);
|
|
354
|
-
for (let i = 0; i < 80; i++) {
|
|
355
|
-
const clicked = await cdpEval(cdp, `(() => {
|
|
356
|
-
const rootPath = ${JSON.stringify(selector)};
|
|
357
|
-
const parts = rootPath.split('>>>');
|
|
358
|
-
let root = document;
|
|
359
|
-
const last = parts.pop().trim();
|
|
360
|
-
for (const raw of parts) {
|
|
361
|
-
const part = raw.trim();
|
|
362
|
-
const node = root === document ? window.__deepFind(part, root) : root.querySelector(part);
|
|
363
|
-
if (!node) return false;
|
|
364
|
-
root = node.shadowRoot || node;
|
|
365
|
-
}
|
|
366
|
-
const el = Array.from(root.querySelectorAll(last)).find((x) => x.textContent.includes(${JSON.stringify(text)}));
|
|
367
|
-
if (!el) return false;
|
|
368
|
-
el.click();
|
|
369
|
-
return true;
|
|
370
|
-
})()`);
|
|
371
|
-
if (clicked) return;
|
|
372
|
-
await wait(100);
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`Text button not found: ${selector} ${text}`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function cdpFill(cdp, selector, value) {
|
|
378
|
-
await installHelper(cdp);
|
|
379
|
-
await cdpEval(cdp, `(() => {
|
|
380
|
-
const el = window.__pq(${JSON.stringify(selector)});
|
|
381
|
-
if (!el) throw new Error("missing ${selector}");
|
|
382
|
-
el.value = ${JSON.stringify(value)};
|
|
383
|
-
el.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
384
|
-
el.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
385
|
-
return true;
|
|
386
|
-
})()`);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function cdpCount(cdp, selector) {
|
|
390
|
-
await installHelper(cdp);
|
|
391
|
-
return cdpEval(cdp, `window.__pqAllCount(${JSON.stringify(selector)})`);
|
|
392
|
-
}
|