Skip to content

Commit e290c90

Browse files
authored
Add tests for hook types (#4108)
* Add tests for hook types Supersedes #4099. Makes the test tsconfig use strict: true and adjusts errors.
1 parent 5a7312a commit e290c90

File tree

12 files changed

+306
-124
lines changed

12 files changed

+306
-124
lines changed

assets/js/phoenix_live_view/live_socket.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ export default class LiveSocket {
221221
this.connect();
222222
}
223223

224+
/**
225+
* @param {HTMLElement} el
226+
* @param {import("./js_commands").EncodedJS} encodedJS
227+
* @param {string | null} [eventType]
228+
*/
224229
execJS(el, encodedJS, eventType = null) {
225230
const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
226231
this.owner(el, (view) => JS.exec(e, eventType, encodedJS, view, el));

assets/js/phoenix_live_view/view_hook.ts

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export interface HookInterface<E extends HTMLElement = HTMLElement> {
159159

160160
// based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fac1aa75acdddbf4f1a95e98ee2297b54ce4b4c9/types/phoenix_live_view/hooks.d.ts#L26
161161
// licensed under MIT
162-
export interface Hook<out T = object, E extends HTMLElement = HTMLElement> {
162+
export interface Hook<T = object, E extends HTMLElement = HTMLElement> {
163163
/**
164164
* The mounted callback.
165165
*
@@ -240,11 +240,15 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
240240
implements HookInterface<E>
241241
{
242242
el: E;
243-
liveSocket: LiveSocket;
244243

245244
private __listeners: Set<CallbackRef>;
246245
private __isDisconnected: boolean;
247-
private __view: () => View;
246+
private __view!: () => View;
247+
private __liveSocket!: () => LiveSocket;
248+
249+
get liveSocket(): LiveSocket {
250+
return this.__liveSocket();
251+
}
248252

249253
static makeID() {
250254
return viewHookID++;
@@ -326,14 +330,18 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
326330
__attachView(view: View | null) {
327331
if (view) {
328332
this.__view = () => view;
329-
this.liveSocket = view.liveSocket;
333+
this.__liveSocket = () => view.liveSocket;
330334
} else {
331335
this.__view = () => {
332336
throw new Error(
333337
`hook not yet attached to a live view: ${this.el.outerHTML}`,
334338
);
335339
};
336-
this.liveSocket = null;
340+
this.__liveSocket = () => {
341+
throw new Error(
342+
`hook not yet attached to a live view: ${this.el.outerHTML}`,
343+
);
344+
};
337345
}
338346
}
339347

@@ -386,43 +394,70 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
386394
};
387395
}
388396

389-
pushEvent(event: string, payload?: any, onReply?: OnReply) {
397+
pushEvent(event: string, payload: any, onReply: OnReply): void;
398+
pushEvent(event: string, payload?: any): Promise<any>;
399+
pushEvent(
400+
event: string,
401+
payload?: any,
402+
onReply?: OnReply,
403+
): Promise<any> | void {
390404
const promise = this.__view().pushHookEvent(
391405
this.el,
392406
null,
393407
event,
394408
payload || {},
395409
);
396410
if (onReply === undefined) {
397-
return promise.then(({ reply }) => reply);
411+
return promise.then(({ reply }: { reply: any }) => reply);
398412
}
399-
promise.then(({ reply, ref }) => onReply(reply, ref)).catch(() => {});
400-
return;
413+
promise
414+
.then(({ reply, ref }: { reply: any; ref: number }) =>
415+
onReply(reply, ref),
416+
)
417+
.catch(() => {});
401418
}
402419

420+
pushEventTo(
421+
selectorOrTarget: PhxTarget,
422+
event: string,
423+
payload: object,
424+
onReply: OnReply,
425+
): void;
426+
pushEventTo(
427+
selectorOrTarget: PhxTarget,
428+
event: string,
429+
payload?: object,
430+
): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]>;
403431
pushEventTo(
404432
selectorOrTarget: PhxTarget,
405433
event: string,
406434
payload?: object,
407435
onReply?: OnReply,
408-
) {
436+
): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]> | void {
409437
if (onReply === undefined) {
410438
const targetPair: { view: View; targetCtx: any }[] = [];
411-
this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
412-
targetPair.push({ view, targetCtx });
413-
});
439+
this.__view().withinTargets(
440+
selectorOrTarget,
441+
(view: View, targetCtx: any) => {
442+
targetPair.push({ view, targetCtx });
443+
},
444+
);
414445
const promises = targetPair.map(({ view, targetCtx }) => {
415446
return view.pushHookEvent(this.el, targetCtx, event, payload || {});
416447
});
417448
return Promise.allSettled(promises);
418449
}
419-
this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
420-
view
421-
.pushHookEvent(this.el, targetCtx, event, payload || {})
422-
.then(({ reply, ref }) => onReply(reply, ref))
423-
.catch(() => {});
424-
});
425-
return;
450+
this.__view().withinTargets(
451+
selectorOrTarget,
452+
(view: View, targetCtx: any) => {
453+
view
454+
.pushHookEvent(this.el, targetCtx, event, payload || {})
455+
.then(({ reply, ref }: { reply: any; ref: number }) =>
456+
onReply(reply, ref),
457+
)
458+
.catch(() => {});
459+
},
460+
);
426461
}
427462

428463
handleEvent(event: string, callback: (payload: any) => any): CallbackRef {
@@ -451,9 +486,12 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
451486
}
452487

453488
uploadTo(selectorOrTarget: PhxTarget, name: string, files: FileList): any {
454-
return this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
455-
view.dispatchUploads(targetCtx, name, files);
456-
});
489+
return this.__view().withinTargets(
490+
selectorOrTarget,
491+
(view: View, targetCtx: any) => {
492+
view.dispatchUploads(targetCtx, name, files);
493+
},
494+
);
457495
}
458496

459497
/** @internal */
@@ -464,6 +502,6 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
464502
}
465503
}
466504

467-
export type HooksOptions = Record<string, typeof ViewHook | Hook>;
505+
export type HooksOptions = Record<string, typeof ViewHook | Hook<any, any>>;
468506

469507
export default ViewHook;

assets/test/debounce_test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe("debounce", function () {
6262
let calls = 0;
6363
const el: HTMLInputElement = container().querySelector(
6464
"input[name=debounce-200]",
65-
);
65+
)!;
6666

6767
el.addEventListener("input", (e) => {
6868
DOM.debounce(
@@ -90,7 +90,7 @@ describe("debounce", function () {
9090
let calls = 0;
9191
const el: HTMLInputElement = container().querySelector(
9292
"input[name=debounce-200]",
93-
);
93+
)!;
9494

9595
el.addEventListener("input", (e) => {
9696
DOM.debounce(
@@ -122,7 +122,7 @@ describe("debounce", function () {
122122
let calls = 0;
123123
const el: HTMLInputElement = container().querySelector(
124124
"input[name=debounce-200]",
125-
);
125+
)!;
126126

127127
el.addEventListener("input", (e) => {
128128
DOM.debounce(
@@ -164,7 +164,7 @@ describe("debounce", function () {
164164
let calls = 0;
165165
const el: HTMLInputElement = container().querySelector(
166166
"input[name=debounce-200]",
167-
);
167+
)!;
168168
el.setAttribute("phx-debounce", "");
169169

170170
el.addEventListener("input", (e) => {
@@ -201,7 +201,7 @@ describe("debounce", function () {
201201
const parent = container();
202202
const el: HTMLInputElement = parent.querySelector(
203203
"input[name=debounce-200]",
204-
);
204+
)!;
205205

206206
el.addEventListener("input", (e) => {
207207
DOM.debounce(
@@ -215,7 +215,7 @@ describe("debounce", function () {
215215
() => calls++,
216216
);
217217
});
218-
el.form.addEventListener("submit", () => {
218+
el.form!.addEventListener("submit", () => {
219219
el.value = "submitted";
220220
});
221221
simulateInput(el, "changed");
@@ -236,7 +236,7 @@ describe("debounce", function () {
236236
describe("throttle", function () {
237237
test("triggers immediately, then on timeout", (done) => {
238238
let calls = 0;
239-
const el: HTMLButtonElement = container().querySelector("#throttle-200");
239+
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;
240240

241241
el.addEventListener("click", (e) => {
242242
DOM.debounce(
@@ -274,7 +274,7 @@ describe("throttle", function () {
274274

275275
test("uses default when value is blank", (done) => {
276276
let calls = 0;
277-
const el: HTMLButtonElement = container().querySelector("#throttle-200");
277+
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;
278278
el.setAttribute("phx-throttle", "");
279279

280280
el.addEventListener("click", (e) => {
@@ -315,7 +315,7 @@ describe("throttle", function () {
315315
let calls = 0;
316316
const el: HTMLInputElement = container().querySelector(
317317
"input[name=throttle-200]",
318-
);
318+
)!;
319319

320320
el.addEventListener("input", (e) => {
321321
DOM.debounce(
@@ -329,7 +329,7 @@ describe("throttle", function () {
329329
() => calls++,
330330
);
331331
});
332-
el.form.addEventListener("submit", () => {
332+
el.form!.addEventListener("submit", () => {
333333
el.value = "submitted";
334334
});
335335
simulateInput(el, "changed");
@@ -347,7 +347,7 @@ describe("throttle", function () {
347347

348348
test("triggers only once when there is only one event", (done) => {
349349
let calls = 0;
350-
const el: HTMLButtonElement = container().querySelector("#throttle-200");
350+
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;
351351

352352
el.addEventListener("click", (e) => {
353353
DOM.debounce(
@@ -377,7 +377,7 @@ describe("throttle", function () {
377377
let calls = 0;
378378
const el: HTMLInputElement = container().querySelector(
379379
"input[name=throttle-range-with-blur]",
380-
);
380+
)!;
381381

382382
el.addEventListener("input", (e) => {
383383
DOM.debounce(
@@ -422,7 +422,7 @@ describe("throttle", function () {
422422
describe("throttle keydown", function () {
423423
test("when the same key is pressed triggers immediately, then on timeout", (done) => {
424424
const keyPresses = {};
425-
const el: HTMLDivElement = container().querySelector("#throttle-keydown");
425+
const el: HTMLDivElement = container().querySelector("#throttle-keydown")!;
426426

427427
el.addEventListener("keydown", (e) => {
428428
DOM.debounce(
@@ -457,7 +457,7 @@ describe("throttle keydown", function () {
457457

458458
test("when different key is pressed triggers immediately", (done) => {
459459
const keyPresses = {};
460-
const el: HTMLDivElement = container().querySelector("#throttle-keydown");
460+
const el: HTMLDivElement = container().querySelector("#throttle-keydown")!;
461461

462462
el.addEventListener("keydown", (e) => {
463463
DOM.debounce(

assets/test/event_test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import LiveSocket from "phoenix_live_view/live_socket";
33
import View from "phoenix_live_view/view";
44

55
import { version as liveview_version } from "../../package.json";
6+
import { HooksOptions } from "phoenix_live_viewview_hook";
67

78
let containerId = 0;
89

@@ -60,7 +61,7 @@ describe("events", () => {
6061

6162
test("events on join", () => {
6263
let liveSocket = new LiveSocket("/live", Socket, {
63-
hooks: {
64+
hooks: <HooksOptions>{
6465
Map: {
6566
mounted() {
6667
this.handleEvent("points", (data) =>
@@ -86,7 +87,7 @@ describe("events", () => {
8687

8788
test("events on update", () => {
8889
let liveSocket = new LiveSocket("/live", Socket, {
89-
hooks: {
90+
hooks: <HooksOptions>{
9091
Game: {
9192
mounted() {
9293
this.handleEvent("scores", (data) =>
@@ -114,9 +115,9 @@ describe("events", () => {
114115
});
115116

116117
test("events handlers are cleaned up on destroy", () => {
117-
let destroyed = [];
118+
let destroyed: Array<string> = [];
118119
let liveSocket = new LiveSocket("/live", Socket, {
119-
hooks: {
120+
hooks: <HooksOptions>{
120121
Handler: {
121122
mounted() {
122123
this.handleEvent("my-event", (data) =>
@@ -164,7 +165,7 @@ describe("events", () => {
164165

165166
test("removeHandleEvent", () => {
166167
let liveSocket = new LiveSocket("/live", Socket, {
167-
hooks: {
168+
hooks: <HooksOptions>{
168169
Remove: {
169170
mounted() {
170171
let ref = this.handleEvent("remove", (data) => {
@@ -202,7 +203,7 @@ describe("pushEvent replies", () => {
202203
test("reply", (done) => {
203204
let view;
204205
let liveSocket = new LiveSocket("/live", Socket, {
205-
hooks: {
206+
hooks: <HooksOptions>{
206207
Gateway: {
207208
mounted() {
208209
stubNextChannelReply(view, { transactionID: "1001" });
@@ -240,7 +241,7 @@ describe("pushEvent replies", () => {
240241
test("promise", (done) => {
241242
let view;
242243
let liveSocket = new LiveSocket("/live", Socket, {
243-
hooks: {
244+
hooks: <HooksOptions>{
244245
Gateway: {
245246
mounted() {
246247
stubNextChannelReply(view, { transactionID: "1001" });
@@ -276,7 +277,7 @@ describe("pushEvent replies", () => {
276277
test("rejects with error", (done) => {
277278
let view;
278279
let liveSocket = new LiveSocket("/live", Socket, {
279-
hooks: {
280+
hooks: <HooksOptions>{
280281
Gateway: {
281282
mounted() {
282283
stubNextChannelReplyWithError(view, "error");
@@ -305,7 +306,7 @@ describe("pushEvent replies", () => {
305306
test("pushEventTo - promise with multiple targets", (done) => {
306307
let view;
307308
let liveSocket = new LiveSocket("/live", Socket, {
308-
hooks: {
309+
hooks: <HooksOptions>{
309310
Gateway: {
310311
mounted() {
311312
stubNextChannelReply(view, { transactionID: "1001" });
@@ -350,7 +351,7 @@ describe("pushEvent replies", () => {
350351
let view;
351352
const spy = jest.fn();
352353
let liveSocket = new LiveSocket("/live", Socket, {
353-
hooks: {
354+
hooks: <HooksOptions>{
354355
Gateway: {
355356
mounted() {
356357
stubNextChannelReply(view, { transactionID: "1001" });

0 commit comments

Comments
 (0)