@emseepea/create-html-ui-server 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  This directory is both the maintained example and its public npm initializer.
4
4
 
5
+ ## Use This Template
6
+
7
+ Use this template for an accessible server-rendered HTML form with the fewest
8
+ front-end dependencies. Choose the [React UI server](../react-ui-server/README.md)
9
+ when the surrounding application already uses React. [Compare all eight templates](https://emseepea.github.io/emseepea/examples/).
10
+
11
+ ## Create a Project
12
+
5
13
  ```sh
6
- npm init @emseepea/html-ui-server@next -- my-server
14
+ npm init @emseepea/html-ui-server -- my-server
7
15
  ```
8
16
 
9
17
  <!-- generated-project-readme -->
@@ -18,6 +26,12 @@ Protocol (MCP) server. It uses the native renderer and the same sample states as
18
26
  the React example. Choose the React example instead when your application
19
27
  already uses React and needs client-side updates.
20
28
 
29
+ `src/server.ts` assembles the server. HTTP handlers live in `src/routes/`, where
30
+ the filename supplies the method and path. `get.index.ts` serves the page,
31
+ `post.index.ts` handles the form, and `get.emseepea.css.ts` serves the
32
+ stylesheet. Add a direct Fastify route only when a handler does not fit this
33
+ simple file convention.
34
+
21
35
  ## Run
22
36
 
23
37
  From this directory:
@@ -32,7 +46,7 @@ Open
32
46
  `http://127.0.0.1:3000/` for the page or use
33
47
  `http://127.0.0.1:3000/mcp` for Model Context Protocol (MCP).
34
48
 
35
- The page previews content only. It does not send or store a report.
49
+ The page previews a sample pea planting plan only. It does not send or store a report.
36
50
 
37
51
  ## Check This Example
38
52
 
@@ -8,6 +8,12 @@ Protocol (MCP) server. It uses the native renderer and the same sample states as
8
8
  the React example. Choose the React example instead when your application
9
9
  already uses React and needs client-side updates.
10
10
 
11
+ `src/server.ts` assembles the server. HTTP handlers live in `src/routes/`, where
12
+ the filename supplies the method and path. `get.index.ts` serves the page,
13
+ `post.index.ts` handles the form, and `get.emseepea.css.ts` serves the
14
+ stylesheet. Add a direct Fastify route only when a handler does not fit this
15
+ simple file convention.
16
+
11
17
  ## Run
12
18
 
13
19
  From this directory:
@@ -22,7 +28,7 @@ Open
22
28
  `http://127.0.0.1:3000/` for the page or use
23
29
  `http://127.0.0.1:3000/mcp` for Model Context Protocol (MCP).
24
30
 
25
- The page previews content only. It does not send or store a report.
31
+ The page previews a sample pea planting plan only. It does not send or store a report.
26
32
 
27
33
  ## Check This Example
28
34
 
@@ -3,19 +3,19 @@ import { toolSelectionTest } from "@emseepea/testing/semantic";
3
3
  toolSelectionTest("Native UI preview is not mistaken for a completed effect", {
4
4
  server: new URL("../dist/server.js", import.meta.url),
5
5
  question:
6
- "Summarize the dark-roast preview. Was a report sent or stored, and did this " +
6
+ "Summarize the snap-pea planting-plan preview. Was a report sent or stored, and did this " +
7
7
  "operation change anything?",
8
8
  criticalFacts: [
9
- "Forest Ember",
10
- "Sample Range",
9
+ "Highland Snap",
10
+ "Meadow Sweet",
11
11
  "preview-only",
12
12
  "false",
13
13
  /\b(?:no|not|nothing|neither|never|wasn't|weren't|didn't)\b[^.!?\n]{0,80}\b(?:sent|delivered)\b/i,
14
14
  /\b(?:no|not|nothing|neither|never|wasn't|weren't|didn't)\b[^.!?\n]{0,80}\b(?:stored|saved|persisted)\b/i
15
15
  ],
16
16
  criteria:
17
- "The answer identifies Forest Ember from Sample Range as the one dark-roast " +
18
- "match. It says the result is preview-only, effectPerformed is false, no report " +
17
+ "The answer identifies Highland Snap and Meadow Sweet as the two snap-pea " +
18
+ "matches. It says the result is preview-only, effectPerformed is false, no report " +
19
19
  "was sent or stored, and no external action or data change occurred.",
20
- expectedTools: ["preview-bean-report"],
20
+ expectedTools: ["preview-planting-plan"],
21
21
  });
@@ -16,7 +16,7 @@
16
16
  "devDependencies": {
17
17
  "playwright": "1.62.1",
18
18
  "axe-core": "4.13.0",
19
- "@emseepea/testing": "0.2.1",
19
+ "@emseepea/testing": "0.2.2",
20
20
  "@types/node": "24.13.3",
21
21
  "typescript": "6.0.3",
22
22
  "oxlint": "1.80.0"
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "private": true,
28
28
  "dependencies": {
29
- "@emseepea/server": "0.2.1",
30
- "@emseepea/tailwind": "0.0.1"
29
+ "@emseepea/server": "0.3.0",
30
+ "@emseepea/tailwind": "0.0.2"
31
31
  }
32
32
  }
@@ -0,0 +1,4 @@
1
+ import { createPreviewPlantingPlanTool } from "../ui-shared.js";
2
+ import type { CapabilityModuleFactory } from "@emseepea/server";
3
+
4
+ export default (() => createPreviewPlantingPlanTool()) satisfies CapabilityModuleFactory;
@@ -0,0 +1,9 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { HttpRouteHandler } from "@emseepea/server";
4
+
5
+ const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
6
+
7
+ export default (async (_request, reply) => {
8
+ await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
9
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { pageFromQuery } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("text/html; charset=utf-8").send(pageFromQuery(request.query));
7
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { pageFromSubmission } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("text/html; charset=utf-8").send(pageFromSubmission(request.body));
7
+ }) satisfies HttpRouteHandler;
@@ -1,17 +1,11 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { parse } from "node:querystring";
3
2
 
4
- import {
5
- fixtureForState,
6
- viewFromSubmission,
7
- } from "./ui-shared.js";
8
- import { createEmseepea, discoverCapabilities, renderElicitationForm, serveEmseepea } from "@emseepea/server";
3
+ import { createEmseepea, discoverCapabilities, registerRoutes, serveEmseepea } from "@emseepea/server";
9
4
 
10
- const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
11
5
  const app = createEmseepea({
12
- name: "emseepea-native-ui",
6
+ name: "emseepea-html-ui-server",
13
7
  version: "0.0.0",
14
- instructions: "Use preview-bean-report to preview sample report content. It sends and stores nothing.",
8
+ instructions: "Use preview-planting-plan to preview a sample pea planting plan. It sends and stores nothing.",
15
9
  ...await discoverCapabilities(new URL("./capabilities/", import.meta.url)),
16
10
  });
17
11
 
@@ -20,20 +14,7 @@ app.addContentTypeParser(
20
14
  { parseAs: "string" },
21
15
  (_request, body, done) => done(null, parse(body.toString())),
22
16
  );
23
- app.get("/emseepea.css", async (_request, reply) => {
24
- await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
25
- });
26
- app.get("/", async (request, reply) => {
27
- const query = record(request.query);
28
- await reply.type("text/html; charset=utf-8").send(page(
29
- fixtureForState(query.state),
30
- first(query.theme) === "dark" ? "dark" : "light",
31
- first(query.style) !== "off",
32
- ));
33
- });
34
- app.post("/", async (request, reply) => {
35
- await reply.type("text/html; charset=utf-8").send(page(viewFromSubmission(request.body), "light", true));
36
- });
17
+ await registerRoutes(app, new URL("./routes/", import.meta.url));
37
18
 
38
19
  const running = await serveEmseepea(app, { port: Number.parseInt(process.env.PORT ?? "3000", 10) });
39
20
  console.log(`Em See Pea native UI example listening at ${running.url}`);
@@ -44,18 +25,3 @@ async function shutdown(): Promise<void> {
44
25
  }
45
26
  process.once("SIGINT", () => void shutdown());
46
27
  process.once("SIGTERM", () => void shutdown());
47
-
48
- function page(view: unknown, theme: "light" | "dark", styled: boolean): string {
49
- return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
50
- `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
51
- `<title>Bean report preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
52
- `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
53
- `<h1>Native form example</h1>${renderElicitationForm(view, { headingLevel: 2 })}</main></body></html>`;
54
- }
55
-
56
- function record(value: unknown): Record<string, unknown> {
57
- return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
58
- }
59
- function first(value: unknown): string | undefined {
60
- return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
61
- }
@@ -1,68 +1,68 @@
1
1
  import { defineElicitationView, defineTool, type ElicitationView } from "@emseepea/server";
2
2
  import { z } from "zod";
3
3
 
4
- const roastSchema = z.enum(["all", "light", "medium", "dark"]);
4
+ const peaTypeSchema = z.enum(["all", "shelling", "snap"]);
5
5
  const previewInputSchema = z.strictObject({
6
6
  title: z.string().trim().min(1).max(80),
7
- roast: roastSchema,
8
- includeNotes: z.boolean(),
7
+ peaType: peaTypeSchema,
8
+ includeTips: z.boolean(),
9
9
  });
10
10
  const previewOutputSchema = z.strictObject({
11
11
  status: z.literal("preview-only"),
12
12
  effectPerformed: z.literal(false),
13
13
  title: z.string(),
14
14
  matchingCount: z.number().int().nonnegative(),
15
- beans: z.array(z.strictObject({
15
+ varieties: z.array(z.strictObject({
16
16
  name: z.string(),
17
- origin: z.string(),
18
- roast: z.enum(["light", "medium", "dark"]),
19
- notes: z.array(z.string()).optional(),
17
+ growthHabit: z.enum(["bush", "climbing"]),
18
+ peaType: z.enum(["shelling", "snap"]),
19
+ tips: z.array(z.string()).optional(),
20
20
  })),
21
21
  notice: z.literal("No report was sent or stored."),
22
22
  });
23
23
 
24
- const beans = [
25
- { name: "Harbour Dawn", origin: "Sample Coast", roast: "light" as const, notes: ["citrus", "honey"] },
26
- { name: "Highland Bloom", origin: "Sample Highlands", roast: "medium" as const, notes: ["berry", "cocoa"] },
27
- { name: "Forest Ember", origin: "Sample Range", roast: "dark" as const, notes: ["molasses", "cedar"] },
24
+ const varieties = [
25
+ { name: "Harbour Gem", growthHabit: "bush" as const, peaType: "shelling" as const, tips: ["compact", "harvest when pods feel full"] },
26
+ { name: "Highland Snap", growthHabit: "climbing" as const, peaType: "snap" as const, tips: ["provide support", "pick pods young"] },
27
+ { name: "Meadow Sweet", growthHabit: "bush" as const, peaType: "snap" as const, tips: ["suits containers", "keep soil moist"] },
28
28
  ];
29
29
 
30
- export function previewBeanReport(input: z.output<typeof previewInputSchema>) {
31
- const matching = beans.filter((bean) => input.roast === "all" || bean.roast === input.roast);
30
+ export function previewPlantingPlan(input: z.output<typeof previewInputSchema>) {
31
+ const matching = varieties.filter((variety) => input.peaType === "all" || variety.peaType === input.peaType);
32
32
  return {
33
33
  status: "preview-only" as const,
34
34
  effectPerformed: false as const,
35
35
  title: input.title,
36
36
  matchingCount: matching.length,
37
- beans: matching.map(({ notes, ...bean }) => input.includeNotes ? { ...bean, notes } : bean),
37
+ varieties: matching.map(({ tips, ...variety }) => input.includeTips ? { ...variety, tips } : variety),
38
38
  notice: "No report was sent or stored." as const,
39
39
  };
40
40
  }
41
41
 
42
- export function createPreviewBeanReportTool() {
42
+ export function createPreviewPlantingPlanTool() {
43
43
  return defineTool({
44
- name: "preview-bean-report",
44
+ name: "preview-planting-plan",
45
45
  access: "public",
46
- title: "Preview a Bean Report",
47
- description: "Preview a sample bean report without sending, storing, or changing anything.",
46
+ title: "Preview a Pea Planting Plan",
47
+ description: "Preview a sample pea planting plan without sending, storing, or changing anything.",
48
48
  inputSchema: previewInputSchema,
49
49
  outputSchema: previewOutputSchema,
50
50
  handler(input) {
51
- const data = previewBeanReport(input);
51
+ const data = previewPlantingPlan(input);
52
52
  return {
53
- text: `${data.title} contains ${data.matchingCount} matching sample beans. ${data.notice}`,
53
+ text: `${data.title} contains ${data.matchingCount} matching pea varieties. ${data.notice}`,
54
54
  data,
55
55
  };
56
56
  },
57
57
  });
58
58
  }
59
59
 
60
- const fields = (values: { title: string; roast: z.output<typeof roastSchema>; includeNotes: boolean }, titleErrors?: string[]) => [
60
+ const fields = (values: { title: string; peaType: z.output<typeof peaTypeSchema>; includeTips: boolean }, titleErrors?: string[]) => [
61
61
  {
62
62
  kind: "text" as const,
63
63
  id: "report-title",
64
64
  name: "title",
65
- label: "Report title",
65
+ label: "Plan title",
66
66
  description: "Name this preview so its purpose is clear.",
67
67
  required: true,
68
68
  minLength: 1,
@@ -72,50 +72,49 @@ const fields = (values: { title: string; roast: z.output<typeof roastSchema>; in
72
72
  },
73
73
  {
74
74
  kind: "select" as const,
75
- id: "roast",
76
- name: "roast",
77
- label: "Roast",
78
- description: "Choose which sample beans the preview includes.",
75
+ id: "pea-type",
76
+ name: "peaType",
77
+ label: "Pea type",
78
+ description: "Choose which sample pea varieties the preview includes.",
79
79
  required: true,
80
- placeholder: "Choose a roast",
81
- value: values.roast,
80
+ placeholder: "Choose a pea type",
81
+ value: values.peaType,
82
82
  options: [
83
- { value: "all", label: "All roasts" },
84
- { value: "light", label: "Light" },
85
- { value: "medium", label: "Medium" },
86
- { value: "dark", label: "Dark" },
83
+ { value: "all", label: "All pea types" },
84
+ { value: "shelling", label: "Shelling" },
85
+ { value: "snap", label: "Snap" },
87
86
  ],
88
87
  },
89
88
  {
90
89
  kind: "checkbox" as const,
91
- id: "include-notes",
92
- name: "includeNotes",
93
- label: "Include tasting notes",
94
- description: "Add sample tasting notes to the preview.",
95
- checked: values.includeNotes,
90
+ id: "include-tips",
91
+ name: "includeTips",
92
+ label: "Include growing tips",
93
+ description: "Add sample growing tips to the preview.",
94
+ checked: values.includeTips,
96
95
  },
97
96
  ];
98
97
 
99
98
  const base = {
100
- id: "bean-report-preview",
101
- heading: "Preview a bean report",
102
- intro: "Review sample report content. This preview sends and stores nothing.",
103
- legend: "Report options",
99
+ id: "pea-planting-plan-preview",
100
+ heading: "Preview a pea planting plan",
101
+ intro: "Review a sample planting plan. This preview sends and stores nothing.",
102
+ legend: "Planting plan options",
104
103
  submitLabel: "Create preview",
105
104
  } as const;
106
- const defaults = { title: "Roast overview", roast: "all" as const, includeNotes: true };
105
+ const defaults = { title: "Autumn pea plan", peaType: "all" as const, includeTips: true };
107
106
 
108
107
  export const elicitationFixtures = {
109
108
  ready: defineElicitationView({ ...base, fields: fields(defaults), state: { kind: "ready", focusTarget: "none" } }),
110
109
  invalid: defineElicitationView({
111
110
  ...base,
112
- fields: fields({ ...defaults, title: "" }, ["Enter a report title."]),
111
+ fields: fields({ ...defaults, title: "" }, ["Enter a plan title."]),
113
112
  state: {
114
113
  kind: "invalid",
115
114
  focusTarget: "error-summary",
116
115
  summary: {
117
- heading: "Fix the report options",
118
- items: [{ fieldId: "report-title", message: "Enter a report title." }],
116
+ heading: "Fix the planting plan options",
117
+ items: [{ fieldId: "report-title", message: "Enter a plan title." }],
119
118
  },
120
119
  },
121
120
  }),
@@ -125,7 +124,7 @@ export const elicitationFixtures = {
125
124
  state: {
126
125
  kind: "busy",
127
126
  focusTarget: "status",
128
- status: "Preparing a sample preview. No report is being sent or stored.",
127
+ status: "Preparing a sample planting-plan preview. No report is being sent or stored.",
129
128
  },
130
129
  }),
131
130
  terminal: defineElicitationView({
@@ -135,7 +134,7 @@ export const elicitationFixtures = {
135
134
  kind: "terminal",
136
135
  focusTarget: "terminal",
137
136
  heading: "Preview ready",
138
- message: "Three sample beans match. No report was sent or stored.",
137
+ message: "Three sample pea varieties match. No report was sent or stored.",
139
138
  },
140
139
  }),
141
140
  } satisfies Readonly<Record<"ready" | "invalid" | "busy" | "terminal", ElicitationView>>;
@@ -149,27 +148,27 @@ export function fixtureForState(value: unknown): ElicitationView {
149
148
  export function viewFromSubmission(value: unknown): ElicitationView {
150
149
  const record = typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
151
150
  const title = firstString(record.title)?.trim() ?? "";
152
- const roast = roastSchema.safeParse(firstString(record.roast));
151
+ const peaType = peaTypeSchema.safeParse(firstString(record.peaType));
153
152
  const input = {
154
153
  title,
155
- roast: roast.success ? roast.data : "all" as const,
156
- includeNotes: firstString(record.includeNotes) === "true",
154
+ peaType: peaType.success ? peaType.data : "all" as const,
155
+ includeTips: firstString(record.includeTips) === "true",
157
156
  };
158
157
  if (!title) {
159
158
  return defineElicitationView({
160
159
  ...base,
161
- fields: fields(input, ["Enter a report title."]),
160
+ fields: fields(input, ["Enter a plan title."]),
162
161
  state: {
163
162
  kind: "invalid",
164
163
  focusTarget: "error-summary",
165
164
  summary: {
166
- heading: "Fix the report options",
167
- items: [{ fieldId: "report-title", message: "Enter a report title." }],
165
+ heading: "Fix the planting plan options",
166
+ items: [{ fieldId: "report-title", message: "Enter a plan title." }],
168
167
  },
169
168
  },
170
169
  });
171
170
  }
172
- const data = previewBeanReport(input);
171
+ const data = previewPlantingPlan(input);
173
172
  return defineElicitationView({
174
173
  ...base,
175
174
  fields: fields(input),
@@ -177,7 +176,7 @@ export function viewFromSubmission(value: unknown): ElicitationView {
177
176
  kind: "terminal",
178
177
  focusTarget: "terminal",
179
178
  heading: "Preview ready",
180
- message: `${data.matchingCount} sample ${data.matchingCount === 1 ? "bean matches" : "beans match"}. ${data.notice}`,
179
+ message: `${data.matchingCount} sample pea ${data.matchingCount === 1 ? "variety matches" : "varieties match"}. ${data.notice}`,
181
180
  },
182
181
  });
183
182
  }
@@ -0,0 +1,31 @@
1
+ import { fixtureForState, viewFromSubmission } from "./ui-shared.js";
2
+ import { renderElicitationForm } from "@emseepea/server";
3
+
4
+ export function pageFromQuery(value: unknown): string {
5
+ const query = record(value);
6
+ return page(
7
+ fixtureForState(query.state),
8
+ first(query.theme) === "dark" ? "dark" : "light",
9
+ first(query.style) !== "off",
10
+ );
11
+ }
12
+
13
+ export function pageFromSubmission(value: unknown): string {
14
+ return page(viewFromSubmission(value), "light", true);
15
+ }
16
+
17
+ function page(view: unknown, theme: "light" | "dark", styled: boolean): string {
18
+ return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
19
+ `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
20
+ `<title>Pea planting plan preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
21
+ `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
22
+ `<h1>Native form example</h1>${renderElicitationForm(view, { headingLevel: 2 })}</main></body></html>`;
23
+ }
24
+
25
+ function record(value: unknown): Record<string, unknown> {
26
+ return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
27
+ }
28
+
29
+ function first(value: unknown): string | undefined {
30
+ return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
31
+ }
@@ -5,7 +5,7 @@ import { fixtureForState, viewFromSubmission } from "../dist/ui-shared.js";
5
5
 
6
6
  test("shared UI fixtures preserve preview-only semantics", () => {
7
7
  assert.equal(fixtureForState("ready").state.kind, "ready");
8
- const result = viewFromSubmission({ title: "Daily roast", roast: "dark", includeNotes: "on" });
8
+ const result = viewFromSubmission({ title: "Autumn peas", peaType: "snap", includeTips: "true" });
9
9
  assert.equal(result.state.kind, "terminal");
10
10
  assert.match(result.state.message, /No report was sent or stored/);
11
11
  });
@@ -23,6 +23,7 @@ export function testUiExample(example) {
23
23
  if (message.type() === "error") errors.push(message.text());
24
24
  });
25
25
  page.on("pageerror", (error) => errors.push(error.message));
26
+ await assertRouteResponses(page.request, running.origin, example.react === true);
26
27
 
27
28
  for (const theme of ["light", "dark"]) {
28
29
  for (const state of ["ready", "invalid", "busy", "terminal"]) {
@@ -35,9 +36,11 @@ export function testUiExample(example) {
35
36
  `${example.name} ${theme} ${state} has axe violations`,
36
37
  );
37
38
  assert.equal(await page.locator("html[lang='en']").count(), 1);
39
+ assert.equal(await page.locator("meta[name='viewport'][content='width=device-width, initial-scale=1']").count(), 1);
40
+ assert.equal(await page.locator("a[href='#main-content']").innerText(), "Skip to main content");
38
41
  assert.equal(await page.locator("main#main-content").count(), 1);
39
42
  assert.equal(await page.locator("h1").count(), 1);
40
- assert.equal(await page.title(), "Bean report preview - Em See Pea");
43
+ assert.equal(await page.title(), "Pea planting plan preview - Em See Pea");
41
44
  assert.equal(await page.locator("h1").innerText(), example.h1);
42
45
  assert.deepEqual(
43
46
  await page.locator("h1,h2,h3,h4,h5,h6").evaluateAll((headings) => headings.map((heading) => ({
@@ -46,8 +49,8 @@ export function testUiExample(example) {
46
49
  }))),
47
50
  [
48
51
  { level: 1, text: example.h1 },
49
- { level: 2, text: "Preview a bean report" },
50
- ...(state === "invalid" ? [{ level: 3, text: "Fix the report options" }] : []),
52
+ { level: 2, text: "Preview a pea planting plan" },
53
+ ...(state === "invalid" ? [{ level: 3, text: "Fix the planting plan options" }] : []),
51
54
  ...(state === "terminal" ? [{ level: 3, text: "Preview ready" }] : []),
52
55
  ],
53
56
  );
@@ -66,29 +69,29 @@ export function testUiExample(example) {
66
69
  assert.equal(await page.evaluate(() => document.activeElement?.id), "main-content");
67
70
 
68
71
  await page.goto(`${running.origin}/?state=invalid`);
69
- await page.waitForFunction(() => document.activeElement?.id === "bean-report-preview--error-summary");
72
+ await page.waitForFunction(() => document.activeElement?.id === "pea-planting-plan-preview--error-summary");
70
73
  assert.equal(await page.locator("[data-emseepea-part='error-summary']").getAttribute("role"), "alert");
71
74
  assert.equal(
72
75
  await page.locator("[data-emseepea-part='error-summary'] a").innerText(),
73
- "Report title: Enter a report title.",
76
+ "Plan title: Enter a plan title.",
74
77
  );
75
78
  assert.equal(
76
79
  await page.locator("[aria-invalid='true']").getAttribute("aria-describedby"),
77
- "bean-report-preview--field--report-title--description bean-report-preview--field--report-title--error",
80
+ "pea-planting-plan-preview--field--report-title--description pea-planting-plan-preview--field--report-title--error",
78
81
  );
79
82
 
80
83
  await page.goto(`${running.origin}/?state=busy`);
81
- await page.waitForFunction(() => document.activeElement?.id === "bean-report-preview--status");
84
+ await page.waitForFunction(() => document.activeElement?.id === "pea-planting-plan-preview--status");
82
85
  assert.equal(await page.locator("form").getAttribute("aria-busy"), "true");
83
86
  assert.equal(await page.locator("button[type='submit']").isDisabled(), true);
84
87
 
85
88
  await page.goto(`${running.origin}/?state=terminal`);
86
- await page.waitForFunction(() => document.activeElement?.id === "bean-report-preview--terminal");
89
+ await page.waitForFunction(() => document.activeElement?.id === "pea-planting-plan-preview--terminal");
87
90
  await assertNoEffectClaim(page);
88
91
 
89
92
  await page.goto(`${running.origin}/?style=off&state=ready`);
90
93
  assert.equal(await page.locator("link[rel='stylesheet']").count(), 0);
91
- assert.equal(await page.locator("label[for='bean-report-preview--field--report-title']").count(), 1);
94
+ assert.equal(await page.locator("label[for='pea-planting-plan-preview--field--report-title']").count(), 1);
92
95
  assert.equal(await page.locator("form").count(), 1);
93
96
 
94
97
  await page.goto(`${running.origin}/?state=ready`);
@@ -130,6 +133,32 @@ export function testUiExample(example) {
130
133
  });
131
134
  }
132
135
 
136
+ async function assertRouteResponses(request, origin, react) {
137
+ const document = await request.get(`${origin}/`);
138
+ assert.equal(document.status(), 200);
139
+ assert.match(document.headers()["content-type"], /^text\/html; charset=utf-8$/);
140
+
141
+ const stylesheet = await request.get(`${origin}/emseepea.css`);
142
+ assert.equal(stylesheet.status(), 200);
143
+ assert.match(stylesheet.headers()["content-type"], /^text\/css; charset=utf-8$/);
144
+ assert.equal(stylesheet.headers()["cache-control"], "no-store");
145
+
146
+ const submission = await request.post(`${origin}/`, { form: { title: "Route contract" } });
147
+ assert.equal(submission.status(), 200);
148
+ assert.match(
149
+ submission.headers()["content-type"],
150
+ react ? /^application\/json; charset=utf-8$/ : /^text\/html; charset=utf-8$/,
151
+ );
152
+
153
+ if (react) {
154
+ const client = await request.get(`${origin}/client.js`);
155
+ assert.equal(client.status(), 200);
156
+ assert.match(client.headers()["content-type"], /^text\/javascript; charset=utf-8$/);
157
+ assert.equal(client.headers()["cache-control"], "no-store");
158
+ assert.ok((await client.body()).byteLength > 0);
159
+ }
160
+ }
161
+
133
162
  async function assertNoEffectClaim(page) {
134
163
  const text = await page.locator("[data-emseepea-part='terminal']").innerText();
135
164
  assert.match(text, /No report was sent or stored\./);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-html-ui-server",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Create an Em See Pea server with an accessible HTML form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,12 +18,12 @@
18
18
  "prepack": "npm run build:initializer"
19
19
  },
20
20
  "devDependencies": {
21
- "@emseepea/server": "0.2.1",
22
- "@emseepea/tailwind": "0.0.1",
21
+ "@emseepea/server": "0.3.0",
22
+ "@emseepea/tailwind": "0.0.2",
23
23
  "@emseepea/example-ui-shared": "0.0.0",
24
24
  "playwright": "1.62.1",
25
25
  "axe-core": "4.13.0",
26
- "@emseepea/testing": "0.2.1",
26
+ "@emseepea/testing": "0.2.2",
27
27
  "@types/node": "24.13.3",
28
28
  "typescript": "6.0.3",
29
29
  "oxlint": "1.80.0"
@@ -31,10 +31,10 @@
31
31
  "engines": {
32
32
  "node": ">=22"
33
33
  },
34
- "repository": { "type": "git", "url": "git+https://github.com/emseepea/emseepea.git", "directory": "examples/native-ui" },
34
+ "repository": { "type": "git", "url": "git+https://github.com/emseepea/emseepea.git", "directory": "examples/html-ui-server" },
35
35
  "homepage": "https://emseepea.github.io/emseepea/examples/",
36
36
  "bugs": "https://github.com/emseepea/emseepea/issues",
37
- "publishConfig": { "access": "public", "provenance": true, "tag": "next" },
37
+ "publishConfig": { "access": "public", "provenance": true },
38
38
  "bin": { "create-html-ui-server": "./initializer-dist/create.mjs" },
39
39
  "files": ["initializer-dist"]
40
40
  }
@@ -1,4 +0,0 @@
1
- import { createPreviewBeanReportTool } from "../ui-shared.js";
2
- import type { CapabilityModuleFactory } from "@emseepea/server";
3
-
4
- export default (() => createPreviewBeanReportTool()) satisfies CapabilityModuleFactory;