@sealcode/sealgen 0.15.22 → 0.15.24

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.
Files changed (36) hide show
  1. package/@types/src/add-collection.d.ts +1 -1
  2. package/@types/src/page/list.d.ts +1 -0
  3. package/@types/src/page/sealious-list.d.ts +1 -1
  4. package/@types/src/page/sealious-list.test.d.ts +1 -0
  5. package/@types/src/test_utils/exec_async.d.ts +1 -0
  6. package/@types/src/test_utils/test-on-real-app.d.ts +24 -0
  7. package/@types/src/utils/input-wrapper.d.ts +1 -1
  8. package/lib/add-collection.js +18 -8
  9. package/lib/add-route.test.js +9 -92
  10. package/lib/forms/controls/checkboxed-list-input.js +31 -22
  11. package/lib/forms/controls/checkboxed-list-input.test.js +173 -0
  12. package/lib/forms/controls/multiple-files.js +1 -0
  13. package/lib/forms/controls/simple-input.js +1 -0
  14. package/lib/forms/controls/tickable.js +1 -0
  15. package/lib/forms/fields/field.js +1 -1
  16. package/lib/page/list.js +1 -0
  17. package/lib/page/sealious-list.js +5 -2
  18. package/lib/page/sealious-list.test.js +24 -0
  19. package/lib/test_utils/exec_async.js +24 -0
  20. package/lib/test_utils/test-on-real-app.js +147 -0
  21. package/lib/utils/input-wrapper.js +4 -2
  22. package/package.json +1 -1
  23. package/src/add-collection.ts +23 -8
  24. package/src/add-route.test.ts +9 -99
  25. package/src/forms/controls/checkboxed-list-input.test.ts +189 -0
  26. package/src/forms/controls/checkboxed-list-input.ts +36 -23
  27. package/src/forms/controls/multiple-files.ts +1 -0
  28. package/src/forms/controls/simple-input.ts +1 -0
  29. package/src/forms/controls/tickable.ts +1 -0
  30. package/src/forms/fields/field.ts +3 -1
  31. package/src/page/list.ts +1 -1
  32. package/src/page/sealious-list.test.ts +25 -0
  33. package/src/page/sealious-list.ts +7 -3
  34. package/src/test_utils/exec_async.ts +26 -0
  35. package/src/test_utils/test-on-real-app.ts +167 -0
  36. package/src/utils/input-wrapper.ts +4 -1
@@ -0,0 +1,147 @@
1
+ import { execAsync } from "./exec_async.js";
2
+ import { spawn } from "child_process";
3
+ import getPort from "get-port";
4
+ import waitPort from "wait-port";
5
+ import { promises as fs } from "fs";
6
+ import _locreq from "locreq";
7
+ import { module_dirname } from "../utils/module-dirname.js";
8
+ import EventEmitter from "events";
9
+ const locreq = _locreq(module_dirname(import.meta.url));
10
+ const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
11
+ const sealious_minimal_commit = "b0d5bf5";
12
+ class RealAppTest {
13
+ constructor() {
14
+ this.emitter = new EventEmitter();
15
+ }
16
+ static async init() {
17
+ const test_app = new RealAppTest();
18
+ test_app.node_pid = 0;
19
+ const timestamp = Date.now();
20
+ test_app.app_path = `/tmp/sealious-minimal-${timestamp}`;
21
+ test_app.container_name = `sealgen-test-mongo-${timestamp}`;
22
+ console.log(" cloning sealious-minimal...");
23
+ await execAsync(
24
+ `git clone https://hub.sealcode.org/source/sealious-minimal.git ${test_app.app_path} && cd ${test_app.app_path} && git checkout ${sealious_minimal_commit}`
25
+ );
26
+ console.log(" installing dependencies and sealgen...");
27
+ await execAsync(
28
+ `cd ${test_app.app_path} && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
29
+ );
30
+ console.log(
31
+ " starting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
32
+ );
33
+ test_app.app_port = await getPort();
34
+ return test_app;
35
+ }
36
+ async start() {
37
+ this.mongo_port = await getPort();
38
+ await execAsync(
39
+ `docker run -p ${this.mongo_port}:27017 --name ${this.container_name} -d mongo:4.4-bionic`
40
+ );
41
+ const env = {
42
+ ...process.env,
43
+ SEALIOUS_MONGO_PORT: this.mongo_port.toString(),
44
+ SEALIOUS_PORT: this.app_port.toString()
45
+ };
46
+ const node_args = [`${this.app_path}/dist/back/index.js`];
47
+ console.log(
48
+ "EXECUTING APP",
49
+ `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join(" ")} node ${node_args.join(" ")}`
50
+ );
51
+ const app_start = spawn(`node`, node_args, {
52
+ env
53
+ });
54
+ app_start.stdout.on(
55
+ "data",
56
+ (data) => console.log("APP OUTPUT:", data.toString())
57
+ );
58
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
59
+ app_start.on("exit", (code) => {
60
+ this.emitter.emit("exit", code);
61
+ this.emitter.emit("process_error");
62
+ });
63
+ if (app_start.pid == void 0)
64
+ throw new Error("node failed to start");
65
+ this.node_pid = app_start.pid;
66
+ await waitPort({
67
+ host: "localhost",
68
+ port: this.app_port,
69
+ output: "silent",
70
+ timeout: 6e3
71
+ });
72
+ }
73
+ async close() {
74
+ try {
75
+ if (this.node_pid != 0)
76
+ await execAsync(`kill -9 ${this.node_pid}`);
77
+ } catch (error) {
78
+ throw new Error(
79
+ "node process exited unexpectedly, please inspect the output"
80
+ );
81
+ }
82
+ await Promise.all([
83
+ fs.rm(this.app_path, { recursive: true, force: true }),
84
+ execAsync(`docker rm -f ${this.container_name}`)
85
+ ]);
86
+ }
87
+ async runSealgenCommand(command) {
88
+ await execAsync(
89
+ `cd ${this.app_path} && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
90
+ );
91
+ }
92
+ async throwIfItKillsTheApp(cb) {
93
+ let finished = false;
94
+ return new Promise(async (resolve, reject) => {
95
+ const error_handler = () => {
96
+ if (!finished) {
97
+ reject();
98
+ }
99
+ finished = true;
100
+ };
101
+ this.emitter.addListener("process_error", error_handler);
102
+ try {
103
+ const result = await cb();
104
+ if (!finished) {
105
+ resolve(result);
106
+ finished = true;
107
+ }
108
+ } catch (e) {
109
+ if (!finished) {
110
+ reject(e);
111
+ finished = true;
112
+ }
113
+ }
114
+ this.emitter.removeListener("process_error", error_handler);
115
+ });
116
+ }
117
+ async httpGet(path) {
118
+ return this.throwIfItKillsTheApp(async () => {
119
+ const ret = await fetch(`http://localhost:${this.app_port}${path}`);
120
+ const status = ret.status;
121
+ const response = await ret.text();
122
+ return { status, response };
123
+ });
124
+ }
125
+ async httpPOST(path, body) {
126
+ return this.throwIfItKillsTheApp(async () => {
127
+ const ret = await fetch(
128
+ `http://localhost:${this.app_port}${path}`,
129
+ {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify(body)
133
+ }
134
+ );
135
+ const status = ret.status;
136
+ const response = await ret.text();
137
+ console.log("request done!");
138
+ console.log({ response });
139
+ return { status, response };
140
+ });
141
+ }
142
+ }
143
+ export {
144
+ RealAppTest,
145
+ callSealgenBin
146
+ };
147
+ //# sourceMappingURL=test-on-real-app.js.map
@@ -5,9 +5,11 @@ function getRequiredClass(isRequired) {
5
5
  function getModifierClasses(modifiers) {
6
6
  return modifiers.map((m) => "form-input__wrapper--" + m).join(" ");
7
7
  }
8
- function inputWrapper(modifiers, input) {
8
+ function inputWrapper(type, modifiers, input) {
9
9
  return tempstream`<div
10
- class="form-input form-input__wrapper ${getModifierClasses(modifiers)}"
10
+ class="form-input form-input--type-${type} form-input__wrapper ${getModifierClasses(
11
+ modifiers
12
+ )}"
11
13
  >
12
14
  ${input}
13
15
  </div>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.15.22",
3
+ "version": "0.15.24",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -1,20 +1,35 @@
1
1
  import { resolve } from "path";
2
2
  import _locreq from "locreq";
3
3
  import { promises as fs, existsSync } from "fs";
4
- import { question } from "./utils/question.js";
5
4
  import { collectionTemplate } from "./templates/collection.js";
6
5
  import { generateCollections } from "./generate-collections.js";
7
6
  import { toKebabCase, toPascalCase } from "js-convert-case";
8
7
  const target_locreq = _locreq(process.cwd());
8
+ import prompts from "prompts";
9
9
 
10
- export async function addCollection(): Promise<void> {
11
- const collection_name = toPascalCase(
12
- await question(
13
- "What's the name of the collection class: ",
14
- (s: string) => s.length > 3
15
- )
16
- );
10
+ export async function addCollection(
11
+ params: Record<string, string | boolean> = {}
12
+ ): Promise<void> {
13
+ prompts.override(params);
14
+ const response = await prompts([
15
+ {
16
+ type: "text",
17
+ name: "collection_name",
18
+ message: "What's the name of the collection class: ",
19
+ validate: (s: string) =>
20
+ s.length > 3 ? true : "Should be at least 3 characters long",
21
+ format: function (action: string) {
22
+ return toPascalCase(action);
23
+ },
24
+ },
25
+ ]);
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28
+ const collection_name: string = response.collection_name;
17
29
 
30
+ if (!collection_name) {
31
+ throw new Error("Empty collection name, please try again");
32
+ }
18
33
  const file_path = target_locreq.resolve(
19
34
  `src/back/collections/${toKebabCase(collection_name)}.ts`
20
35
  );
@@ -1,106 +1,16 @@
1
- import { promises as fs } from "fs";
2
- import { exec, spawn } from "child_process";
3
- import axios from "axios";
4
- import waitPort from "wait-port";
5
- import getPort from "get-port";
6
-
7
- import _locreq from "locreq";
8
- import { module_dirname } from "./utils/module-dirname.js";
9
- const locreq = _locreq(module_dirname(import.meta.url));
10
-
11
- const execAsync = (cmd: string) => {
12
- return new Promise<void>((resolve, reject) => {
13
- const process = exec(cmd);
14
-
15
- console.log("EXECUTING", cmd);
16
-
17
- process?.stdout?.on("data", (data) => {
18
- console.log(`stdout: ${data}`);
19
- });
20
-
21
- process?.stderr?.on("data", (data) => {
22
- console.log(`stdout: ${data}`);
23
- });
24
-
25
- process.on("exit", (code) => {
26
- if (code == 0) {
27
- resolve();
28
- } else {
29
- reject();
30
- }
31
- });
32
- });
33
- };
34
-
35
- const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
1
+ import { RealAppTest } from "./test_utils/test-on-real-app.js";
36
2
 
37
3
  describe("add route test", () => {
38
- let node_pid = 0;
39
- const timestamp = Date.now();
40
- const app_path = `/tmp/sealious-minimal-${timestamp}`;
41
- const container_name = `sealgen-test-mongo-${timestamp}`;
42
- afterEach(async function () {
43
- try {
44
- if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
45
- } catch (error) {
46
- throw new Error("node process exited unexpectedly, please inspect the output");
47
- }
48
- await Promise.all([
49
- fs.rm(app_path, { recursive: true, force: true }),
50
- execAsync(`docker rm -f ${container_name}`),
51
- ]);
52
- });
4
+ it("creates minimal sealious app and adds a simple route that works", async function () {
5
+ const test_app = await RealAppTest.init();
53
6
 
54
- it("creates minimal sealious app", async function () {
55
- console.log("\tcloning sealious-minimal...");
56
- await execAsync(
57
- `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout df8cccf`
58
- );
59
- console.log("\tinstalling dependencies and sealgen...");
60
- await execAsync(
61
- `cd ${app_path} && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
62
- );
63
- console.log("\trunning add-route and building app...");
64
- await execAsync(
65
- `cd ${app_path} && ${callSealgenBin()} add-route --url=/sample --action=Sample --mode=page && ${callSealgenBin()} build`
66
- );
67
- console.log(
68
- "\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
69
- );
70
- const mongo_port = await getPort();
71
- const app_port = await getPort();
72
- await execAsync(
73
- `docker run -p ${mongo_port}:27017 --name ${container_name} -d mongo:4.4-bionic`
74
- );
75
- console.log("\trunning app and making request to the generated url...");
76
- const env = {
77
- ...process.env,
78
- SEALIOUS_MONGO_PORT: mongo_port.toString(),
79
- SEALIOUS_PORT: app_port.toString(),
80
- };
81
- const node_args = [`${app_path}/dist/back/index.js`];
82
- console.log(
83
- "EXECUTING APP",
84
- `${Object.entries(env)
85
- .map(([key, value]) => `${key}=${value}`)
86
- .join(" ")} node ${node_args.join(" ")}`
87
- );
88
- let app_start = spawn(`node`, node_args, {
89
- env,
90
- });
91
- app_start.stdout.on("data", (data) => console.log("APP OUTPUT:", data.toString()));
92
- app_start.stderr.on("data", (data) => console.log(data.toString()));
93
- if (app_start.pid == undefined) throw new Error("node failed to start");
94
- node_pid = app_start.pid;
95
- await waitPort({
96
- host: "localhost",
97
- port: app_port,
98
- output: "silent",
99
- timeout: 6000,
100
- });
101
- const res = await axios.get(`http://localhost:${app_port}/sample`);
7
+ const path = "/sample";
8
+ await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
9
+ await test_app.start();
10
+ const res = await test_app.httpGet(path);
102
11
  if (res.status != 200) {
103
- throw new Error("url generated by add-route exists but doesnt return 200");
12
+ throw new Error("url generated by add-route exists but doesn't return 200");
104
13
  }
14
+ await test_app.close();
105
15
  }).timeout(100 * 1000);
106
16
  });
@@ -9,6 +9,7 @@ import { Form } from "../form.js";
9
9
  import { CheckboxedListInput } from "./checkboxed-list-input.js";
10
10
  import { getBrowser } from "../../utils/browser-creator.js";
11
11
  import assert from "assert";
12
+ import { sleep } from "../../utils/sleep.js";
12
13
 
13
14
  describe("checkboxed-list", () => {
14
15
  describe("basic tests", async () => {
@@ -66,5 +67,193 @@ describe("checkboxed-list", () => {
66
67
  await page.getByRole("button", { name: "Wyślij" }).click();
67
68
  assert.deepStrictEqual(received_values, { syllables: { co: "on", de: "on" } });
68
69
  });
70
+
71
+ describe("behavior with (non)required", () => {
72
+ it("allows to send a form when no value is selected and fiels is not required", async () => {
73
+ const port = await getPort();
74
+ console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
75
+ const app = new Koa();
76
+ const router = new Router();
77
+
78
+ const fields = {
79
+ syllables: new CheckboxedListField(false, async () => {
80
+ await sleep(10);
81
+ return ["sa", "mple", "co", "de"].map((s) => ({
82
+ value: s,
83
+ label: s.toLocaleUpperCase(),
84
+ }));
85
+ }),
86
+ };
87
+
88
+ let submit_called = false;
89
+
90
+ mount(
91
+ router,
92
+ "/",
93
+ new (class extends Form<typeof fields, unknown> {
94
+ fields = fields;
95
+ controls = [new CheckboxedListInput(fields.syllables)];
96
+
97
+ async onSubmit(ctx: Context) {
98
+ submit_called = true;
99
+ }
100
+
101
+ async canAccess(_: Context) {
102
+ return { canAccess: true, message: "" };
103
+ }
104
+ })(),
105
+ true
106
+ );
107
+ app.use(router.routes()).use(router.allowedMethods());
108
+ server = app.listen(port);
109
+
110
+ browser = await getBrowser();
111
+ const context = await browser.newContext();
112
+ page = await context.newPage();
113
+ await page.goto(`http://localhost:${port}`);
114
+ await page.getByRole("button", { name: "Wyślij" }).click();
115
+ await sleep(100);
116
+ assert(submit_called);
117
+ });
118
+
119
+ it("does not allow to send a form when no value is selected and field IS required", async () => {
120
+ const port = await getPort();
121
+ console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
122
+ const app = new Koa();
123
+ const router = new Router();
124
+
125
+ const fields = {
126
+ syllables: new CheckboxedListField(true, async () => {
127
+ await sleep(10);
128
+ return ["sa", "mple", "co", "de"].map((s) => ({
129
+ value: s,
130
+ label: s.toLocaleUpperCase(),
131
+ }));
132
+ }),
133
+ };
134
+
135
+ let submit_called = false;
136
+
137
+ mount(
138
+ router,
139
+ "/",
140
+ new (class extends Form<typeof fields, unknown> {
141
+ fields = fields;
142
+ controls = [new CheckboxedListInput(fields.syllables)];
143
+
144
+ async onSubmit(ctx: Context) {
145
+ submit_called = true;
146
+ }
147
+
148
+ async canAccess(_: Context) {
149
+ return { canAccess: true, message: "" };
150
+ }
151
+ })(),
152
+ true
153
+ );
154
+ app.use(router.routes()).use(router.allowedMethods());
155
+ server = app.listen(port);
156
+
157
+ browser = await getBrowser();
158
+ const context = await browser.newContext();
159
+ page = await context.newPage();
160
+ await page.goto(`http://localhost:${port}`);
161
+ await page.getByRole("button", { name: "Wyślij" }).click();
162
+ await sleep(100);
163
+ assert(submit_called == false);
164
+ await page.getByText(/empty value/).click({ timeout: 500 }); // should display an error message for that field
165
+ });
166
+
167
+ it("allows to send a form when no value is selected and field IS required", async () => {
168
+ const port = await getPort();
169
+ console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
170
+ const app = new Koa();
171
+ const router = new Router();
172
+
173
+ const fields = {
174
+ syllables: new CheckboxedListField(true, async () => {
175
+ await sleep(10);
176
+ return ["sa", "mple", "co", "de"].map((s) => ({
177
+ value: s,
178
+ label: s.toLocaleUpperCase(),
179
+ }));
180
+ }),
181
+ };
182
+
183
+ let submit_called = false;
184
+
185
+ mount(
186
+ router,
187
+ "/",
188
+ new (class extends Form<typeof fields, unknown> {
189
+ fields = fields;
190
+ controls = [new CheckboxedListInput(fields.syllables)];
191
+
192
+ async onSubmit(ctx: Context) {
193
+ submit_called = true;
194
+ }
195
+
196
+ async canAccess(_: Context) {
197
+ return { canAccess: true, message: "" };
198
+ }
199
+ })(),
200
+ true
201
+ );
202
+ app.use(router.routes()).use(router.allowedMethods());
203
+ server = app.listen(port);
204
+
205
+ browser = await getBrowser();
206
+ const context = await browser.newContext();
207
+ page = await context.newPage();
208
+ await page.goto(`http://localhost:${port}`);
209
+ await page.getByLabel("DE").check();
210
+ await page.getByRole("button", { name: "Wyślij" }).click();
211
+ await sleep(100);
212
+ assert(submit_called);
213
+ });
214
+ });
215
+
216
+ it("renders the label", async () => {
217
+ const port = await getPort();
218
+ console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
219
+ const app = new Koa();
220
+ const router = new Router();
221
+
222
+ const fields = {
223
+ syllables: new CheckboxedListField(true, async () => {
224
+ return ["sa", "mple", "co", "de"].map((s) => ({
225
+ value: s,
226
+ label: s.toLocaleUpperCase(),
227
+ }));
228
+ }),
229
+ };
230
+
231
+ mount(
232
+ router,
233
+ "/",
234
+ new (class extends Form<typeof fields, unknown> {
235
+ fields = fields;
236
+ controls = [new CheckboxedListInput(fields.syllables, { label: "ABC" })];
237
+
238
+ async onSubmit(ctx: Context) {
239
+ // noop
240
+ }
241
+
242
+ async canAccess(_: Context) {
243
+ return { canAccess: true, message: "" };
244
+ }
245
+ })(),
246
+ true
247
+ );
248
+ app.use(router.routes()).use(router.allowedMethods());
249
+ server = app.listen(port);
250
+
251
+ browser = await getBrowser();
252
+ const context = await browser.newContext();
253
+ page = await context.newPage();
254
+ await page.goto(`http://localhost:${port}`);
255
+ await page.getByRole("button", { name: "Wyślij" }).click();
256
+ await page.getByText(/ABC/).click({ timeout: 500 }); // should display an error message for that field
257
+ });
69
258
  });
70
259
  });
@@ -13,10 +13,11 @@ export class CheckboxedListInput extends FormFieldControl {
13
13
  }
14
14
 
15
15
  async render(fctx: FormControlContext) {
16
- const { parsed: currentValue } = await this.field.getValue(
17
- fctx.ctx,
18
- fctx.data
19
- );
16
+ const {
17
+ valid,
18
+ message,
19
+ parsed: currentValue,
20
+ } = await this.field.getValue(fctx.ctx, fctx.data, true);
20
21
  const pickedValues = Object.entries(currentValue || {})
21
22
  .filter(([, is_on]) => is_on)
22
23
  .map(([key]) => key);
@@ -26,24 +27,36 @@ export class CheckboxedListInput extends FormFieldControl {
26
27
  ]);
27
28
 
28
29
  const { name, required } = this.field;
29
- return tempstream/* HTML */ `${isVisible
30
- ? options.map(({ value, label }) =>
31
- inputWrapper(
32
- [name, getRequiredClass(required)],
33
- /* HTML */ `
34
- <input
35
- type="checkbox"
36
- id="${name}.${value}"
37
- name="${name}.${value}"
38
- ${pickedValues.includes(value) ? "checked" : ""}
39
- form=${fctx.form_id}
40
- />
41
- <label for="${name}.${value}"
42
- >${label || value}</label
43
- >
44
- `
45
- )
46
- )
47
- : ""}`;
30
+ return inputWrapper(
31
+ "checkboxed-list",
32
+ [name, getRequiredClass(required)],
33
+ tempstream/* HTML */ `<label
34
+ >${this.options.label || this.field.name}</label
35
+ >${isVisible
36
+ ? options.map(({ value, label }) =>
37
+ inputWrapper(
38
+ "checkbox",
39
+ [name],
40
+ /* HTML */ `
41
+ <input
42
+ type="checkbox"
43
+ id="${name}.${value}"
44
+ name="${name}.${value}"
45
+ ${pickedValues.includes(value)
46
+ ? "checked"
47
+ : ""}
48
+ form=${fctx.form_id}
49
+ />
50
+ <label for="${name}.${value}"
51
+ >${label || value}</label
52
+ >
53
+ `
54
+ )
55
+ )
56
+ : ""}
57
+ ${~valid && fctx.validate
58
+ ? `<div class="input__error">${message}</div>`
59
+ : ""}`
60
+ );
48
61
  }
49
62
  }
@@ -230,6 +230,7 @@ export class MultipleFiles extends FormFieldControl {
230
230
  ][];
231
231
 
232
232
  return inputWrapper(
233
+ "multiple-files",
233
234
  ["multiple-files", this.field.name, ...this.getClassModifiers()],
234
235
  tempstream/* HTML */ `
235
236
  <label>${this.options.label || this.field.name}</label>
@@ -137,6 +137,7 @@ export class SimpleInput<
137
137
  true
138
138
  );
139
139
  return inputWrapper(
140
+ "simple-input",
140
141
  this.getWrapperModifiers(),
141
142
  tempstream/* HTML */ `
142
143
  ${label ? `<label for="${id}">${label}</label>` : ""}
@@ -85,6 +85,7 @@ export abstract class Tickable<T> extends FormFieldControl {
85
85
  const required = this.field.required;
86
86
 
87
87
  return inputWrapper(
88
+ this.type,
88
89
  this.getWrapperClasses(fctx.ctx, fctx.data, parsed),
89
90
 
90
91
  /* HTML */ `
@@ -130,7 +130,9 @@ export abstract class FormField<
130
130
  if (raw == undefined || raw == null || raw == "") {
131
131
  return {
132
132
  parsed: null,
133
- message: this.required ? "empty value" : "",
133
+ message: this.required
134
+ ? "empty value, but field is required"
135
+ : "",
134
136
  raw,
135
137
  ...(validate ? { valid: !this.required } : {}),
136
138
  };
package/src/page/list.ts CHANGED
@@ -14,7 +14,7 @@ import { FormDataValue } from "../forms/form-types.js";
14
14
  export const BasePagePropsShape = <const>{};
15
15
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
16
16
 
17
- const DEFAULT_ITEMS_PER_PAGE = 12;
17
+ export const DEFAULT_ITEMS_PER_PAGE = 12;
18
18
 
19
19
  export const BaseListPageFields = <const>{
20
20
  page: new NumberField(false, 1),
@@ -0,0 +1,25 @@
1
+ import assert from "assert";
2
+ import { RealAppTest } from "../test_utils/test-on-real-app.js";
3
+
4
+ describe("sealious-list test", () => {
5
+ it("creates minimal sealious app and adds a simple route that works", async function () {
6
+ const test_app = await RealAppTest.init();
7
+
8
+ const url = "/list";
9
+ await test_app.runSealgenCommand(`add-collection --collection_name=samples`);
10
+ await test_app.runSealgenCommand(
11
+ `add-route --url=${url} --action=List --mode=list --collection=samples`
12
+ );
13
+ await test_app.start();
14
+ const res_post = await test_app.httpPOST("/api/v1/collections/samples", {
15
+ content: "some-content",
16
+ });
17
+ assert(res_post.status == 201);
18
+ const res = await test_app.httpGet(url);
19
+ if (res.status != 200) {
20
+ throw new Error("url generated by add-route exists but doesn't return 200");
21
+ }
22
+ assert(res.response.includes("some-content"));
23
+ await test_app.close();
24
+ }).timeout(100 * 1000);
25
+ });