editor_download_test.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { act, assertEquals, cleanup, render } from "./setup.ts";
  2. import { EditorMode } from "../../islands/Editor.tsx";
  3. import Editor from "../../islands/Editor.tsx";
  4. function setupMocks() {
  5. const clickSpy: { href: string; download: string }[] = [];
  6. const createObjectURLCalls: string[] = [];
  7. let revokeObjectURLCalled = false;
  8. const originalCreateElement = document.createElement.bind(document);
  9. document.createElement = (tag: string) => {
  10. const el = originalCreateElement(tag) as HTMLAnchorElement;
  11. if (tag === "a") {
  12. el.click = () => {
  13. clickSpy.push({ href: el.href, download: el.download });
  14. };
  15. }
  16. return el;
  17. };
  18. const origCreateObjectURL = URL.createObjectURL;
  19. const origRevokeObjectURL = URL.revokeObjectURL;
  20. URL.createObjectURL = () => {
  21. createObjectURLCalls.push("called");
  22. return "blob:fake-url";
  23. };
  24. URL.revokeObjectURL = () => {
  25. revokeObjectURLCalled = true;
  26. };
  27. const origFetch = globalThis.fetch;
  28. globalThis.fetch = (() =>
  29. Promise.resolve(
  30. new Response("/* mock css */", {
  31. status: 200,
  32. headers: { "Content-Type": "text/css" },
  33. }),
  34. )) as typeof fetch;
  35. const listeners: { type: string; listener: EventListener }[] = [];
  36. const origAddEventListener = globalThis.addEventListener;
  37. const origRemoveEventListener = globalThis.removeEventListener;
  38. globalThis.addEventListener = (
  39. type: string,
  40. listener: EventListenerOrEventListenerObject,
  41. ) => {
  42. listeners.push({
  43. type,
  44. listener: listener as EventListener,
  45. });
  46. return origAddEventListener.call(globalThis, type, listener);
  47. };
  48. globalThis.removeEventListener = (
  49. type: string,
  50. listener: EventListenerOrEventListenerObject,
  51. ) => {
  52. const idx = listeners.findIndex((l) =>
  53. l.type === type && l.listener === (listener as EventListener)
  54. );
  55. if (idx !== -1) listeners.splice(idx, 1);
  56. return origRemoveEventListener.call(globalThis, type, listener);
  57. };
  58. const emitEvent = (type: string, detail: string) => {
  59. const match = listeners.filter((l) => l.type === type);
  60. for (const { listener } of match) {
  61. listener({ type, detail } as unknown as Event);
  62. }
  63. };
  64. return {
  65. clickSpy,
  66. createObjectURLCalls,
  67. get revokeObjectURLCalled() {
  68. return revokeObjectURLCalled;
  69. },
  70. emitEvent,
  71. getListenerCount(type: string) {
  72. return listeners.filter((l) => l.type === type).length;
  73. },
  74. restore() {
  75. document.createElement = originalCreateElement;
  76. URL.createObjectURL = origCreateObjectURL;
  77. URL.revokeObjectURL = origRevokeObjectURL;
  78. globalThis.fetch = origFetch;
  79. globalThis.addEventListener = origAddEventListener;
  80. globalThis.removeEventListener = origRemoveEventListener;
  81. },
  82. };
  83. }
  84. Deno.test({
  85. name:
  86. "Editor - DownloadRequest event triggers download with correct filename",
  87. fn() {
  88. const mocks = setupMocks();
  89. render(<Editor id="test1" content="# Hello" allowMode={EditorMode.Read} />);
  90. act(() => {
  91. mocks.emitEvent("DownloadRequest", "My Post");
  92. });
  93. assertEquals(mocks.clickSpy.length, 1);
  94. assertEquals(mocks.clickSpy[0].download, "My Post.md");
  95. assertEquals(mocks.createObjectURLCalls.length, 1);
  96. assertEquals(mocks.revokeObjectURLCalled, true);
  97. mocks.restore();
  98. cleanup();
  99. },
  100. sanitizeResources: false,
  101. sanitizeOps: false,
  102. });
  103. Deno.test({
  104. name: "Editor - DownloadRequest uses Untitled for empty title",
  105. fn() {
  106. const mocks = setupMocks();
  107. render(
  108. <Editor id="test2" content="Some content" allowMode={EditorMode.Read} />,
  109. );
  110. act(() => {
  111. mocks.emitEvent("DownloadRequest", "");
  112. });
  113. assertEquals(mocks.clickSpy.length, 1);
  114. assertEquals(mocks.clickSpy[0].download, "Untitled.md");
  115. mocks.restore();
  116. cleanup();
  117. },
  118. sanitizeResources: false,
  119. sanitizeOps: false,
  120. });
  121. Deno.test({
  122. name: "Editor - removes DownloadRequest listener on unmount",
  123. fn() {
  124. const mocks = setupMocks();
  125. const { unmount } = render(
  126. <Editor id="test3" content="test" allowMode={EditorMode.Read} />,
  127. );
  128. assertEquals(mocks.getListenerCount("DownloadRequest"), 1);
  129. act(() => {
  130. unmount();
  131. });
  132. assertEquals(mocks.getListenerCount("DownloadRequest"), 0);
  133. mocks.restore();
  134. cleanup();
  135. },
  136. sanitizeResources: false,
  137. sanitizeOps: false,
  138. });