Skip to content

Commit 067f18e

Browse files
add js tests
1 parent 82b209d commit 067f18e

File tree

4 files changed

+271
-5
lines changed

4 files changed

+271
-5
lines changed

src/django_unicorn/static/unicorn/js/messageSender.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function send(component, callback) {
5353

5454
if (value instanceof File) {
5555
formData.append(key, value);
56-
} else if (value instanceof FileList && value.length > 0) {
56+
} else if (typeof FileList !== "undefined" && value instanceof FileList && value.length > 0) {
5757
Array.from(value).forEach((file, index) => {
5858
formData.append(`${key}[${index}]`, file);
5959
});
@@ -68,7 +68,7 @@ export function send(component, callback) {
6868
const value = action.payload.value;
6969
if (value instanceof File) {
7070
formData.append(fieldName, value);
71-
} else if (value instanceof FileList && value.length > 0) {
71+
} else if (typeof FileList !== "undefined" && value instanceof FileList && value.length > 0) {
7272
Array.from(value).forEach((file, index) => {
7373
formData.append(`${fieldName}[${index}]`, file);
7474
});
@@ -362,7 +362,7 @@ function hasFiles(component) {
362362
for (const key in component.data) {
363363
const value = component.data[key];
364364

365-
if (value instanceof File || (value instanceof FileList && value.length > 0)) {
365+
if (value instanceof File || (typeof FileList !== "undefined" && value instanceof FileList && value.length > 0)) {
366366
return true;
367367
}
368368

@@ -380,7 +380,7 @@ function hasFiles(component) {
380380
if (!action.payload) return false;
381381

382382
return Object.values(action.payload).some(
383-
(v) => (v instanceof File) || (v instanceof FileList && v.length > 0)
383+
(v) => (v instanceof File) || (typeof FileList !== "undefined" && v instanceof FileList && v.length > 0)
384384
);
385385
});
386386
}

src/django_unicorn/static/unicorn/js/unicorn.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import test from "ava";
2+
import { getComponent } from "../utils.js";
3+
import { send } from "../../../src/django_unicorn/static/unicorn/js/messageSender.js";
4+
5+
function makeResponse() {
6+
return {
7+
ok: true,
8+
json: async () => ({
9+
id: "",
10+
dom: "",
11+
data: {},
12+
errors: {},
13+
redirect: {},
14+
return: {},
15+
}),
16+
};
17+
}
18+
19+
test("send uses JSON body when no files are present", async (t) => {
20+
const component = getComponent();
21+
component.actionQueue.push({
22+
type: "callMethod",
23+
payload: { name: "save" },
24+
});
25+
26+
let capturedOptions = null;
27+
global.fetch = async (_url, options) => {
28+
capturedOptions = options;
29+
return makeResponse();
30+
};
31+
32+
await send(component);
33+
34+
t.false(capturedOptions.body instanceof FormData);
35+
t.is(capturedOptions.headers["Content-Type"], "application/json");
36+
});
37+
38+
test("send uses FormData when component.data contains a File", async (t) => {
39+
const file = new File(["content"], "photo.jpg", { type: "image/jpeg" });
40+
const component = getComponent(undefined, undefined, undefined, {
41+
photo: file,
42+
});
43+
component.actionQueue.push({
44+
type: "callMethod",
45+
payload: { name: "save" },
46+
});
47+
48+
let capturedOptions = null;
49+
global.fetch = async (_url, options) => {
50+
capturedOptions = options;
51+
return makeResponse();
52+
};
53+
54+
await send(component);
55+
56+
t.true(capturedOptions.body instanceof FormData);
57+
});
58+
59+
test("send omits Content-Type header when using FormData", async (t) => {
60+
const file = new File(["content"], "photo.jpg", { type: "image/jpeg" });
61+
const component = getComponent(undefined, undefined, undefined, {
62+
photo: file,
63+
});
64+
component.actionQueue.push({
65+
type: "callMethod",
66+
payload: { name: "save" },
67+
});
68+
69+
let capturedOptions = null;
70+
global.fetch = async (_url, options) => {
71+
capturedOptions = options;
72+
return makeResponse();
73+
};
74+
75+
await send(component);
76+
77+
t.false("Content-Type" in capturedOptions.headers);
78+
});
79+
80+
test("send uses FormData when a syncInput action payload contains a File", async (t) => {
81+
const file = new File(["content"], "photo.jpg", { type: "image/jpeg" });
82+
const component = getComponent();
83+
component.actionQueue.push({
84+
type: "syncInput",
85+
payload: { name: "photo", value: file },
86+
});
87+
88+
let capturedOptions = null;
89+
global.fetch = async (_url, options) => {
90+
capturedOptions = options;
91+
return makeResponse();
92+
};
93+
94+
await send(component);
95+
96+
t.true(capturedOptions.body instanceof FormData);
97+
t.false("Content-Type" in capturedOptions.headers);
98+
});
99+
100+
test("send FormData includes body field as JSON-stringified payload", async (t) => {
101+
const file = new File(["content"], "photo.jpg", { type: "image/jpeg" });
102+
const component = getComponent(undefined, undefined, undefined, {
103+
photo: file,
104+
});
105+
component.actionQueue.push({
106+
type: "callMethod",
107+
payload: { name: "save" },
108+
});
109+
110+
let capturedBody = null;
111+
global.fetch = async (_url, options) => {
112+
capturedBody = options.body;
113+
return makeResponse();
114+
};
115+
116+
await send(component);
117+
118+
const bodyJson = capturedBody.get("body");
119+
t.is(typeof bodyJson, "string");
120+
121+
const parsed = JSON.parse(bodyJson);
122+
t.is(parsed.id, component.id);
123+
t.truthy(parsed.actionQueue);
124+
});
125+
126+
test("send FormData appends File from component.data", async (t) => {
127+
const file = new File(["hello"], "photo.jpg", { type: "image/jpeg" });
128+
const component = getComponent(undefined, undefined, undefined, {
129+
photo: file,
130+
});
131+
component.actionQueue.push({
132+
type: "callMethod",
133+
payload: { name: "save" },
134+
});
135+
136+
let capturedBody = null;
137+
global.fetch = async (_url, options) => {
138+
capturedBody = options.body;
139+
return makeResponse();
140+
};
141+
142+
await send(component);
143+
144+
const uploadedFile = capturedBody.get("photo");
145+
t.true(uploadedFile instanceof File);
146+
t.is(uploadedFile.name, "photo.jpg");
147+
});
148+
149+
test("send FormData appends File from syncInput action payload", async (t) => {
150+
const file = new File(["content"], "upload.pdf", { type: "application/pdf" });
151+
const component = getComponent();
152+
component.actionQueue.push({
153+
type: "syncInput",
154+
payload: { name: "document", value: file },
155+
});
156+
157+
let capturedBody = null;
158+
global.fetch = async (_url, options) => {
159+
capturedBody = options.body;
160+
return makeResponse();
161+
};
162+
163+
await send(component);
164+
165+
const uploadedFile = capturedBody.get("document");
166+
t.true(uploadedFile instanceof File);
167+
t.is(uploadedFile.name, "upload.pdf");
168+
});
169+
170+
test("send does not call fetch when action queue is empty", async (t) => {
171+
const component = getComponent();
172+
173+
let fetchCalled = false;
174+
global.fetch = async () => {
175+
fetchCalled = true;
176+
return makeResponse();
177+
};
178+
179+
await send(component);
180+
181+
t.false(fetchCalled);
182+
});
183+
184+
test("send uses JSON body when only callMethod actions with no files", async (t) => {
185+
const component = getComponent();
186+
component.actionQueue.push({
187+
type: "callMethod",
188+
payload: { name: "processData" },
189+
});
190+
component.actionQueue.push({
191+
type: "syncInput",
192+
payload: { name: "name", value: "hello" },
193+
});
194+
195+
let capturedOptions = null;
196+
global.fetch = async (_url, options) => {
197+
capturedOptions = options;
198+
return makeResponse();
199+
};
200+
201+
await send(component);
202+
203+
t.false(capturedOptions.body instanceof FormData);
204+
t.is(capturedOptions.headers["Content-Type"], "application/json");
205+
});

tests/js/element/file.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import test from "ava";
2+
import { getElement } from "../utils.js";
3+
4+
test("file input captures accept attribute in file property", (t) => {
5+
const html = "<input unicorn:model='photo' type='file' accept='image/*'></input>";
6+
const element = getElement(html);
7+
8+
t.is(element.file.accept, "image/*");
9+
});
10+
11+
test("file input captures multiple=true when multiple attribute is present", (t) => {
12+
const html = "<input unicorn:model='photo' type='file' multiple></input>";
13+
const element = getElement(html);
14+
15+
t.true(element.file.multiple);
16+
});
17+
18+
test("file input captures multiple=false when multiple attribute is absent", (t) => {
19+
const html = "<input unicorn:model='photo' type='file'></input>";
20+
const element = getElement(html);
21+
22+
t.false(element.file.multiple);
23+
});
24+
25+
test("file input accept defaults to empty string when accept attribute is absent", (t) => {
26+
const html = "<input unicorn:model='photo' type='file'></input>";
27+
const element = getElement(html);
28+
29+
t.is(element.file.accept, "");
30+
});
31+
32+
test("file input sets both accept and multiple from attributes", (t) => {
33+
const html =
34+
"<input unicorn:model='docs' type='file' accept='.pdf,.doc' multiple></input>";
35+
const element = getElement(html);
36+
37+
t.is(element.file.accept, ".pdf,.doc");
38+
t.true(element.file.multiple);
39+
});
40+
41+
test("text input has empty file property", (t) => {
42+
const html = "<input unicorn:model='name' type='text'></input>";
43+
const element = getElement(html);
44+
45+
t.deepEqual(element.file, {});
46+
});
47+
48+
test("file input without unicorn model has empty file property", (t) => {
49+
const html = "<input type='file' accept='image/*'></input>";
50+
const element = getElement(html);
51+
52+
t.deepEqual(element.file, {});
53+
});
54+
55+
test("file input getValue() returns the el.files property", (t) => {
56+
const html = "<input unicorn:model='photo' type='file'></input>";
57+
const element = getElement(html);
58+
59+
// In JSDOM, el.files is an empty FileList (not null)
60+
t.is(element.getValue(), element.el.files);
61+
});

0 commit comments

Comments
 (0)